diff --git a/archunit-example/example-junit4/build.gradle b/archunit-example/example-junit4/build.gradle index 7b216adf83..fb682a99d8 100644 --- a/archunit-example/example-junit4/build.gradle +++ b/archunit-example/example-junit4/build.gradle @@ -1,5 +1,6 @@ plugins { id 'archunit.java-examples-conventions' + id 'archunit.self-test-conventions' } ext.moduleName = 'com.tngtech.archunit.example.junit4' @@ -15,4 +16,4 @@ test { excludeCategories 'com.tngtech.archunit.exampletest.junit4.Example' } } -} \ No newline at end of file +} diff --git a/archunit-example/example-junit4/src/archTest/java/com/tngtech/archunit/exampletest/junit4/ArchUnitArchitectureTest.java b/archunit-example/example-junit4/src/archTest/java/com/tngtech/archunit/exampletest/junit4/ArchUnitArchitectureTest.java new file mode 100644 index 0000000000..995c10594d --- /dev/null +++ b/archunit-example/example-junit4/src/archTest/java/com/tngtech/archunit/exampletest/junit4/ArchUnitArchitectureTest.java @@ -0,0 +1,18 @@ +package com.tngtech.archunit.exampletest.junit4; + +import com.tngtech.archunit.ArchUnitExampleArchitectureRules; +import com.tngtech.archunit.junit.AnalyzeClasses; +import com.tngtech.archunit.junit.ArchTest; +import com.tngtech.archunit.junit.ArchTests; +import com.tngtech.archunit.junit.ArchUnitRunner; +import org.junit.runner.RunWith; + +@RunWith(ArchUnitRunner.class) +@AnalyzeClasses(packages = "com.tngtech.archunit.exampletest.junit4") +public class ArchUnitArchitectureTest { + private ArchUnitArchitectureTest() { + } + + @ArchTest + public static final ArchTests example_rules = ArchTests.in(ArchUnitExampleArchitectureRules.class); +} diff --git a/archunit-example/example-junit5/build.gradle b/archunit-example/example-junit5/build.gradle index a0bac6e726..9743969098 100644 --- a/archunit-example/example-junit5/build.gradle +++ b/archunit-example/example-junit5/build.gradle @@ -1,5 +1,6 @@ plugins { id 'archunit.java-examples-conventions' + id 'archunit.self-test-conventions' } ext.moduleName = 'com.tngtech.archunit.example.junit5' diff --git a/archunit-example/example-junit5/src/archTest/java/com/tngtech/archunit/exampletest/junit5/ArchUnitArchitectureTest.java b/archunit-example/example-junit5/src/archTest/java/com/tngtech/archunit/exampletest/junit5/ArchUnitArchitectureTest.java new file mode 100644 index 0000000000..0cd22237c6 --- /dev/null +++ b/archunit-example/example-junit5/src/archTest/java/com/tngtech/archunit/exampletest/junit5/ArchUnitArchitectureTest.java @@ -0,0 +1,15 @@ +package com.tngtech.archunit.exampletest.junit5; + +import com.tngtech.archunit.ArchUnitExampleArchitectureRules; +import com.tngtech.archunit.junit.AnalyzeClasses; +import com.tngtech.archunit.junit.ArchTest; +import com.tngtech.archunit.junit.ArchTests; + +@AnalyzeClasses(packages = "com.tngtech.archunit.exampletest.junit5") +public class ArchUnitArchitectureTest { + private ArchUnitArchitectureTest() { + } + + @ArchTest + public static final ArchTests example_rules = ArchTests.in(ArchUnitExampleArchitectureRules.class); +} diff --git a/archunit-example/example-junit6/build.gradle b/archunit-example/example-junit6/build.gradle new file mode 100644 index 0000000000..42ecf902ef --- /dev/null +++ b/archunit-example/example-junit6/build.gradle @@ -0,0 +1,20 @@ +plugins { + id 'archunit.java-examples-conventions' + id 'archunit.self-test-conventions' +} + +ext.moduleName = 'com.tngtech.archunit.example.junit6' +ext.minimumJavaVersion = JavaVersion.VERSION_17 + +dependencies { + testImplementation project(path: ':archunit-junit6') + testImplementation project(path: ':archunit-example:example-plain') +} + +test { + useJUnitPlatform { + if (!project.hasProperty('example')) { + excludeTags 'example' + } + } +} diff --git a/archunit-example/example-junit6/src/archTest/java/com/tngtech/archunit/exampletest/junit6/ArchUnitArchitectureTest.java b/archunit-example/example-junit6/src/archTest/java/com/tngtech/archunit/exampletest/junit6/ArchUnitArchitectureTest.java new file mode 100644 index 0000000000..a4ec652c6f --- /dev/null +++ b/archunit-example/example-junit6/src/archTest/java/com/tngtech/archunit/exampletest/junit6/ArchUnitArchitectureTest.java @@ -0,0 +1,15 @@ +package com.tngtech.archunit.exampletest.junit6; + +import com.tngtech.archunit.ArchUnitExampleArchitectureRules; +import com.tngtech.archunit.junit.AnalyzeClasses; +import com.tngtech.archunit.junit.ArchTest; +import com.tngtech.archunit.junit.ArchTests; + +@AnalyzeClasses(packages = "com.tngtech.archunit.exampletest.junit6") +public class ArchUnitArchitectureTest { + private ArchUnitArchitectureTest() { + } + + @ArchTest + public static final ArchTests example_rules = ArchTests.in(ArchUnitExampleArchitectureRules.class); +} diff --git a/archunit-example/example-junit6/src/test/java/com/tngtech/archunit/exampletest/junit6/CodingRulesTest.java b/archunit-example/example-junit6/src/test/java/com/tngtech/archunit/exampletest/junit6/CodingRulesTest.java new file mode 100644 index 0000000000..6bbc358270 --- /dev/null +++ b/archunit-example/example-junit6/src/test/java/com/tngtech/archunit/exampletest/junit6/CodingRulesTest.java @@ -0,0 +1,58 @@ +package com.tngtech.archunit.exampletest.junit6; + +import java.util.logging.Logger; + +import com.tngtech.archunit.core.domain.JavaClasses; +import com.tngtech.archunit.junit.AnalyzeClasses; +import com.tngtech.archunit.junit.ArchTag; +import com.tngtech.archunit.junit.ArchTest; +import com.tngtech.archunit.lang.ArchRule; +import com.tngtech.archunit.lang.CompositeArchRule; + +import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.fields; +import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses; +import static com.tngtech.archunit.library.GeneralCodingRules.ACCESS_STANDARD_STREAMS; +import static com.tngtech.archunit.library.GeneralCodingRules.NO_CLASSES_SHOULD_ACCESS_STANDARD_STREAMS; +import static com.tngtech.archunit.library.GeneralCodingRules.NO_CLASSES_SHOULD_THROW_GENERIC_EXCEPTIONS; +import static com.tngtech.archunit.library.GeneralCodingRules.NO_CLASSES_SHOULD_USE_FIELD_INJECTION; +import static com.tngtech.archunit.library.GeneralCodingRules.NO_CLASSES_SHOULD_USE_JAVA_UTIL_LOGGING; +import static com.tngtech.archunit.library.GeneralCodingRules.NO_CLASSES_SHOULD_USE_JODATIME; + +@ArchTag("example") +@AnalyzeClasses(packages = "com.tngtech.archunit.example.layers") +public class CodingRulesTest { + + @ArchTest + private final ArchRule no_access_to_standard_streams = NO_CLASSES_SHOULD_ACCESS_STANDARD_STREAMS; + + @ArchTest + private void no_access_to_standard_streams_as_method(JavaClasses classes) { + noClasses().should(ACCESS_STANDARD_STREAMS).check(classes); + } + + @ArchTest + private final ArchRule no_generic_exceptions = NO_CLASSES_SHOULD_THROW_GENERIC_EXCEPTIONS; + + @ArchTest + private final ArchRule no_java_util_logging = NO_CLASSES_SHOULD_USE_JAVA_UTIL_LOGGING; + + @ArchTest + private final ArchRule loggers_should_be_private_static_final = + fields().that().haveRawType(Logger.class) + .should().bePrivate() + .andShould().beStatic() + .andShould().beFinal() + .because("we agreed on this convention"); + + @ArchTest + private final ArchRule no_jodatime = NO_CLASSES_SHOULD_USE_JODATIME; + + @ArchTest + private final ArchRule no_field_injection = NO_CLASSES_SHOULD_USE_FIELD_INJECTION; + + @ArchTest + static final ArchRule no_classes_should_access_standard_streams_or_throw_generic_exceptions = + CompositeArchRule.of(NO_CLASSES_SHOULD_ACCESS_STANDARD_STREAMS) + .and(NO_CLASSES_SHOULD_THROW_GENERIC_EXCEPTIONS); + +} diff --git a/archunit-example/example-junit6/src/test/java/com/tngtech/archunit/exampletest/junit6/ControllerRulesTest.java b/archunit-example/example-junit6/src/test/java/com/tngtech/archunit/exampletest/junit6/ControllerRulesTest.java new file mode 100644 index 0000000000..3224302c32 --- /dev/null +++ b/archunit-example/example-junit6/src/test/java/com/tngtech/archunit/exampletest/junit6/ControllerRulesTest.java @@ -0,0 +1,58 @@ +package com.tngtech.archunit.exampletest.junit6; + +import com.tngtech.archunit.base.DescribedPredicate; +import com.tngtech.archunit.core.domain.JavaClass; +import com.tngtech.archunit.core.domain.JavaMember; +import com.tngtech.archunit.core.domain.PackageMatchers; +import com.tngtech.archunit.example.layers.security.Secured; +import com.tngtech.archunit.junit.AnalyzeClasses; +import com.tngtech.archunit.junit.ArchTag; +import com.tngtech.archunit.junit.ArchTest; +import com.tngtech.archunit.lang.ArchRule; + +import static com.tngtech.archunit.core.domain.JavaClass.Functions.GET_PACKAGE_NAME; +import static com.tngtech.archunit.core.domain.JavaMember.Predicates.declaredIn; +import static com.tngtech.archunit.core.domain.properties.CanBeAnnotated.Predicates.annotatedWith; +import static com.tngtech.archunit.lang.conditions.ArchPredicates.are; +import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes; + +@ArchTag("example") +@AnalyzeClasses(packages = "com.tngtech.archunit.example.layers") +public class ControllerRulesTest { + + @ArchTest + static final ArchRule controllers_should_only_call_secured_methods = + classes().that().resideInAPackage("..controller..") + .should().onlyCallMethodsThat(areDeclaredInController().or(are(annotatedWith(Secured.class)))); + + @ArchTest + static final ArchRule controllers_should_only_call_secured_constructors = + classes() + .that().resideInAPackage("..controller..") + .should().onlyCallConstructorsThat(areDeclaredInController().or(are(annotatedWith(Secured.class)))); + + @ArchTest + static final ArchRule controllers_should_only_call_secured_code_units = + classes() + .that().resideInAPackage("..controller..") + .should().onlyCallCodeUnitsThat(areDeclaredInController().or(are(annotatedWith(Secured.class)))); + + @ArchTest + static final ArchRule controllers_should_only_access_secured_fields = + classes() + .that().resideInAPackage("..controller..") + .should().onlyAccessFieldsThat(areDeclaredInController().or(are(annotatedWith(Secured.class)))); + + @ArchTest + static final ArchRule controllers_should_only_access_secured_members = + classes() + .that().resideInAPackage("..controller..") + .should().onlyAccessMembersThat(areDeclaredInController().or(are(annotatedWith(Secured.class)))); + + private static DescribedPredicate areDeclaredInController() { + DescribedPredicate aPackageController = GET_PACKAGE_NAME.is(PackageMatchers.of("..controller..", "java..")) + .as("a package '..controller..'"); + return are(declaredIn(aPackageController)); + } + +} diff --git a/archunit-example/example-junit6/src/test/java/com/tngtech/archunit/exampletest/junit6/CyclicDependencyRulesTest.java b/archunit-example/example-junit6/src/test/java/com/tngtech/archunit/exampletest/junit6/CyclicDependencyRulesTest.java new file mode 100644 index 0000000000..2d2003a185 --- /dev/null +++ b/archunit-example/example-junit6/src/test/java/com/tngtech/archunit/exampletest/junit6/CyclicDependencyRulesTest.java @@ -0,0 +1,82 @@ +package com.tngtech.archunit.exampletest.junit6; + +import com.tngtech.archunit.core.domain.JavaClass; +import com.tngtech.archunit.example.cycles.complexcycles.slice1.SliceOneCallingConstructorInSliceTwoAndMethodInSliceThree; +import com.tngtech.archunit.example.cycles.complexcycles.slice3.ClassCallingConstructorInSliceFive; +import com.tngtech.archunit.junit.AnalyzeClasses; +import com.tngtech.archunit.junit.ArchTag; +import com.tngtech.archunit.junit.ArchTest; +import com.tngtech.archunit.lang.ArchRule; +import com.tngtech.archunit.library.dependencies.SliceAssignment; +import com.tngtech.archunit.library.dependencies.SliceIdentifier; + +import static com.tngtech.archunit.base.DescribedPredicate.alwaysTrue; +import static com.tngtech.archunit.core.domain.JavaClass.Predicates.resideInAPackage; +import static com.tngtech.archunit.library.dependencies.SlicesRuleDefinition.slices; + +@ArchTag("example") +@AnalyzeClasses(packages = "com.tngtech.archunit.example.cycles") +public class CyclicDependencyRulesTest { + + @ArchTest + static final ArchRule no_cycles_by_method_calls_between_slices = + slices().matching("..(simplecycle).(*)..").namingSlices("$2 of $1").should().beFreeOfCycles(); + + @ArchTest + static final ArchRule no_cycles_by_constructor_calls_between_slices = + slices().matching("..(constructorcycle).(*)..").namingSlices("$2 of $1").should().beFreeOfCycles(); + + @ArchTest + static final ArchRule no_cycles_by_inheritance_between_slices = + slices().matching("..(inheritancecycle).(*)..").namingSlices("$2 of $1").should().beFreeOfCycles(); + + @ArchTest + static final ArchRule no_cycles_by_field_access_between_slices = + slices().matching("..(fieldaccesscycle).(*)..").namingSlices("$2 of $1").should().beFreeOfCycles(); + + @ArchTest + static final ArchRule no_cycles_by_member_dependencies_between_slices = + slices().matching("..(membercycle).(*)..").namingSlices("$2 of $1").should().beFreeOfCycles(); + + @ArchTest + static final ArchRule no_cycles_in_simple_scenario = + slices().matching("..simplescenario.(*)..").namingSlices("$1").should().beFreeOfCycles(); + + @ArchTest + static final ArchRule no_cycles_in_complex_scenario = + slices().matching("..(complexcycles).(*)..").namingSlices("$2 of $1").should().beFreeOfCycles(); + + @ArchTest + static final ArchRule no_cycles_in_complex_scenario_with_custom_ignore = + slices().matching("..(complexcycles).(*)..").namingSlices("$2 of $1") + .as("Slices of complex scenario ignoring some violations") + .should().beFreeOfCycles() + .ignoreDependency(SliceOneCallingConstructorInSliceTwoAndMethodInSliceThree.class, ClassCallingConstructorInSliceFive.class) + .ignoreDependency(resideInAPackage("..slice4.."), alwaysTrue()); + + @ArchTest + static final ArchRule no_cycles_in_freely_customized_slices = + slices().assignedFrom(inComplexSliceOneOrTwo()) + .namingSlices("$1[$2]") + .should().beFreeOfCycles(); + + private static SliceAssignment inComplexSliceOneOrTwo() { + return new SliceAssignment() { + @Override + public String getDescription() { + return "complex slice one or two"; + } + + @Override + public SliceIdentifier getIdentifierOf(JavaClass javaClass) { + if (javaClass.getPackageName().contains("complexcycles.slice1")) { + return SliceIdentifier.of("Complex-Cycle", "One"); + } + if (javaClass.getPackageName().contains("complexcycles.slice2")) { + return SliceIdentifier.of("Complex-Cycle", "Two"); + } + return SliceIdentifier.ignore(); + } + }; + } +} diff --git a/archunit-example/example-junit6/src/test/java/com/tngtech/archunit/exampletest/junit6/DaoRulesTest.java b/archunit-example/example-junit6/src/test/java/com/tngtech/archunit/exampletest/junit6/DaoRulesTest.java new file mode 100644 index 0000000000..38600cc287 --- /dev/null +++ b/archunit-example/example-junit6/src/test/java/com/tngtech/archunit/exampletest/junit6/DaoRulesTest.java @@ -0,0 +1,41 @@ +package com.tngtech.archunit.exampletest.junit6; + +import java.sql.SQLException; + +import javax.persistence.Entity; +import javax.persistence.EntityManager; + +import com.tngtech.archunit.junit.AnalyzeClasses; +import com.tngtech.archunit.junit.ArchTag; +import com.tngtech.archunit.junit.ArchTest; +import com.tngtech.archunit.lang.ArchRule; + +import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes; +import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses; +import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noMethods; + +@ArchTag("example") +@AnalyzeClasses(packages = "com.tngtech.archunit.example.layers") +public class DaoRulesTest { + @ArchTest + static final ArchRule DAOs_must_reside_in_a_dao_package = + classes().that().haveNameMatching(".*Dao").should().resideInAPackage("..dao..") + .as("DAOs should reside in a package '..dao..'"); + + @ArchTest + static final ArchRule entities_must_reside_in_a_domain_package = + classes().that().areAnnotatedWith(Entity.class).should().resideInAPackage("..domain..") + .as("Entities should reside in a package '..domain..'"); + + @ArchTest + static final ArchRule only_DAOs_may_use_the_EntityManager = + noClasses().that().resideOutsideOfPackage("..dao..") + .should().accessClassesThat().areAssignableTo(EntityManager.class) + .as("Only DAOs may use the " + EntityManager.class.getSimpleName()); + + + @ArchTest + static final ArchRule DAOs_must_not_throw_SQLException = + noMethods().that().areDeclaredInClassesThat().haveNameMatching(".*Dao") + .should().declareThrowableOfType(SQLException.class); +} diff --git a/archunit-example/example-junit6/src/test/java/com/tngtech/archunit/exampletest/junit6/DependencyRulesTest.java b/archunit-example/example-junit6/src/test/java/com/tngtech/archunit/exampletest/junit6/DependencyRulesTest.java new file mode 100644 index 0000000000..32062e42ad --- /dev/null +++ b/archunit-example/example-junit6/src/test/java/com/tngtech/archunit/exampletest/junit6/DependencyRulesTest.java @@ -0,0 +1,17 @@ +package com.tngtech.archunit.exampletest.junit6; + +import com.tngtech.archunit.example.layers.ClassViolatingCodingRules; +import com.tngtech.archunit.junit.AnalyzeClasses; +import com.tngtech.archunit.junit.ArchTag; +import com.tngtech.archunit.junit.ArchTest; +import com.tngtech.archunit.lang.ArchRule; + +import static com.tngtech.archunit.library.DependencyRules.NO_CLASSES_SHOULD_DEPEND_UPPER_PACKAGES; + +@ArchTag("example") +@AnalyzeClasses(packagesOf = ClassViolatingCodingRules.class) +public class DependencyRulesTest { + + @ArchTest + static final ArchRule no_accesses_to_upper_package = NO_CLASSES_SHOULD_DEPEND_UPPER_PACKAGES; +} diff --git a/archunit-example/example-junit6/src/test/java/com/tngtech/archunit/exampletest/junit6/FrozenRulesTest.java b/archunit-example/example-junit6/src/test/java/com/tngtech/archunit/exampletest/junit6/FrozenRulesTest.java new file mode 100644 index 0000000000..8faed7fed4 --- /dev/null +++ b/archunit-example/example-junit6/src/test/java/com/tngtech/archunit/exampletest/junit6/FrozenRulesTest.java @@ -0,0 +1,39 @@ +package com.tngtech.archunit.exampletest.junit6; + +import javax.persistence.EntityManager; + +import com.tngtech.archunit.ArchConfiguration; +import com.tngtech.archunit.junit.AnalyzeClasses; +import com.tngtech.archunit.junit.ArchTag; +import com.tngtech.archunit.junit.ArchTest; +import com.tngtech.archunit.lang.ArchRule; +import com.tngtech.archunit.library.freeze.FreezingArchRule; +import com.tngtech.archunit.library.freeze.ViolationLineMatcher; +import com.tngtech.archunit.library.freeze.ViolationStore; + +import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses; +import static com.tngtech.archunit.library.freeze.FreezingArchRule.freeze; + +/** + * This test demonstrates the use of {@link FreezingArchRule} with 'default' configuration. + * While both rules shown have numerous violations, most of those violations have been 'frozen', i.e. at some point in time all violations + * were recorded as accepted for the moment. Only violations added afterwards will be reported.
+ * You can see how the default text based {@link ViolationStore} stores the results under {@code src/test/resources/frozen} configured + * via {@value ArchConfiguration#ARCHUNIT_PROPERTIES_RESOURCE_NAME}. You can also + * observe that if you fix an old violation, this store will automatically be adjusted to not allow any regression.
+ * Furthermore you can observe how the default {@link ViolationLineMatcher} will ignore changes in line numbers of recorded violations, + * i.e. if you only change the line numbers of frozen violations, the test will still pass. + */ +@ArchTag("example") +@AnalyzeClasses(packages = "com.tngtech.archunit.example.layers") +public class FrozenRulesTest { + + @ArchTest + static final ArchRule no_classes_should_depend_on_service = + freeze(noClasses().should().dependOnClassesThat().resideInAPackage("..service..")); + + @ArchTest + static final ArchRule no_classes_should_use_the_EntityManager = + freeze(noClasses().should().dependOnClassesThat().areAssignableTo(EntityManager.class)); +} + diff --git a/archunit-example/example-junit6/src/test/java/com/tngtech/archunit/exampletest/junit6/InterfaceRulesTest.java b/archunit-example/example-junit6/src/test/java/com/tngtech/archunit/exampletest/junit6/InterfaceRulesTest.java new file mode 100644 index 0000000000..c0836e83b7 --- /dev/null +++ b/archunit-example/example-junit6/src/test/java/com/tngtech/archunit/exampletest/junit6/InterfaceRulesTest.java @@ -0,0 +1,45 @@ +package com.tngtech.archunit.exampletest.junit6; + +import java.util.HashSet; +import java.util.Set; + +import com.tngtech.archunit.core.importer.Location; +import com.tngtech.archunit.core.importer.Locations; +import com.tngtech.archunit.example.layers.SomeBusinessInterface; +import com.tngtech.archunit.example.layers.persistence.first.dao.SomeDao; +import com.tngtech.archunit.example.layers.service.impl.SomeInterfacePlacedInTheWrongPackage; +import com.tngtech.archunit.junit.AnalyzeClasses; +import com.tngtech.archunit.junit.ArchTag; +import com.tngtech.archunit.junit.ArchTest; +import com.tngtech.archunit.junit.LocationProvider; +import com.tngtech.archunit.lang.ArchRule; + +import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses; + +@ArchTag("example") +@AnalyzeClasses(locations = InterfaceRulesTest.RelevantExampleClasses.class) +public class InterfaceRulesTest { + + @ArchTest + static final ArchRule interfaces_should_not_have_names_ending_with_the_word_interface = + noClasses().that().areInterfaces().should().haveNameMatching(".*Interface"); + + @ArchTest + static final ArchRule interfaces_should_not_have_simple_class_names_containing_the_word_interface = + noClasses().that().areInterfaces().should().haveSimpleNameContaining("Interface"); + + @ArchTest + static final ArchRule interfaces_must_not_be_placed_in_implementation_packages = + noClasses().that().resideInAPackage("..impl..").should().beInterfaces(); + + public static class RelevantExampleClasses implements LocationProvider { + @Override + public Set get(Class testClass) { + Set result = new HashSet<>(); + result.addAll(Locations.ofClass(SomeBusinessInterface.class)); + result.addAll(Locations.ofClass(SomeDao.class)); + result.addAll(Locations.ofClass(SomeInterfacePlacedInTheWrongPackage.class)); + return result; + } + } +} diff --git a/archunit-example/example-junit6/src/test/java/com/tngtech/archunit/exampletest/junit6/LayerDependencyRulesTest.java b/archunit-example/example-junit6/src/test/java/com/tngtech/archunit/exampletest/junit6/LayerDependencyRulesTest.java new file mode 100644 index 0000000000..acc4f9c849 --- /dev/null +++ b/archunit-example/example-junit6/src/test/java/com/tngtech/archunit/exampletest/junit6/LayerDependencyRulesTest.java @@ -0,0 +1,59 @@ +package com.tngtech.archunit.exampletest.junit6; + +import com.tngtech.archunit.junit.AnalyzeClasses; +import com.tngtech.archunit.junit.ArchTag; +import com.tngtech.archunit.junit.ArchTest; +import com.tngtech.archunit.lang.ArchRule; + +import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes; +import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses; + +@ArchTag("example") +@AnalyzeClasses(packages = "com.tngtech.archunit.example.layers") +public class LayerDependencyRulesTest { + + // 'access' catches only violations by real accesses, i.e. accessing a field, calling a method; compare 'dependOn' further down + + @ArchTest + static final ArchRule services_should_not_access_controllers = + noClasses().that().resideInAPackage("..service..") + .should().accessClassesThat().resideInAPackage("..controller.."); + + @ArchTest + static final ArchRule persistence_should_not_access_services = + noClasses().that().resideInAPackage("..persistence..") + .should().accessClassesThat().resideInAPackage("..service.."); + + @ArchTest + static final ArchRule services_should_only_be_accessed_by_controllers_or_other_services = + classes().that().resideInAPackage("..service..") + .should().onlyBeAccessed().byAnyPackage("..controller..", "..service.."); + + @ArchTest + static final ArchRule services_should_only_access_persistence_or_other_services = + classes().that().resideInAPackage("..service..") + .should().onlyAccessClassesThat().resideInAnyPackage("..service..", "..persistence..", "java.."); + + // 'dependOn' catches a wider variety of violations, e.g. having fields of type, having method parameters of type, extending type ... + + @ArchTest + static final ArchRule services_should_not_depend_on_controllers = + noClasses().that().resideInAPackage("..service..") + .should().dependOnClassesThat().resideInAPackage("..controller.."); + + @ArchTest + static final ArchRule persistence_should_not_depend_on_services = + noClasses().that().resideInAPackage("..persistence..") + .should().dependOnClassesThat().resideInAPackage("..service.."); + + @ArchTest + static final ArchRule services_should_only_be_depended_on_by_controllers_or_other_services = + classes().that().resideInAPackage("..service..") + .should().onlyHaveDependentClassesThat().resideInAnyPackage("..controller..", "..service.."); + + @ArchTest + static final ArchRule services_should_only_depend_on_persistence_or_other_services = + classes().that().resideInAPackage("..service..") + .should().onlyDependOnClassesThat().resideInAnyPackage("..service..", "..persistence..", "java..", "javax.."); + +} diff --git a/archunit-example/example-junit6/src/test/java/com/tngtech/archunit/exampletest/junit6/LayeredArchitectureTest.java b/archunit-example/example-junit6/src/test/java/com/tngtech/archunit/exampletest/junit6/LayeredArchitectureTest.java new file mode 100644 index 0000000000..5f75cf81a6 --- /dev/null +++ b/archunit-example/example-junit6/src/test/java/com/tngtech/archunit/exampletest/junit6/LayeredArchitectureTest.java @@ -0,0 +1,38 @@ +package com.tngtech.archunit.exampletest.junit6; + +import com.tngtech.archunit.example.layers.SomeMediator; +import com.tngtech.archunit.example.layers.service.ServiceViolatingLayerRules; +import com.tngtech.archunit.junit.AnalyzeClasses; +import com.tngtech.archunit.junit.ArchTag; +import com.tngtech.archunit.junit.ArchTest; +import com.tngtech.archunit.lang.ArchRule; + +import static com.tngtech.archunit.library.Architectures.layeredArchitecture; + +@ArchTag("example") +@AnalyzeClasses(packages = "com.tngtech.archunit.example.layers") +public class LayeredArchitectureTest { + @ArchTest + static final ArchRule layer_dependencies_are_respected = layeredArchitecture().consideringAllDependencies() + + .layer("Controllers").definedBy("com.tngtech.archunit.example.layers.controller..") + .layer("Services").definedBy("com.tngtech.archunit.example.layers.service..") + .layer("Persistence").definedBy("com.tngtech.archunit.example.layers.persistence..") + + .whereLayer("Controllers").mayNotBeAccessedByAnyLayer() + .whereLayer("Services").mayOnlyBeAccessedByLayers("Controllers") + .whereLayer("Persistence").mayOnlyBeAccessedByLayers("Services"); + + @ArchTest + static final ArchRule layer_dependencies_are_respected_with_exception = layeredArchitecture().consideringAllDependencies() + + .layer("Controllers").definedBy("com.tngtech.archunit.example.layers.controller..") + .layer("Services").definedBy("com.tngtech.archunit.example.layers.service..") + .layer("Persistence").definedBy("com.tngtech.archunit.example.layers.persistence..") + + .whereLayer("Controllers").mayNotBeAccessedByAnyLayer() + .whereLayer("Services").mayOnlyBeAccessedByLayers("Controllers") + .whereLayer("Persistence").mayOnlyBeAccessedByLayers("Services") + + .ignoreDependency(SomeMediator.class, ServiceViolatingLayerRules.class); +} diff --git a/archunit-example/example-junit6/src/test/java/com/tngtech/archunit/exampletest/junit6/MethodsTest.java b/archunit-example/example-junit6/src/test/java/com/tngtech/archunit/exampletest/junit6/MethodsTest.java new file mode 100644 index 0000000000..fa6b660d22 --- /dev/null +++ b/archunit-example/example-junit6/src/test/java/com/tngtech/archunit/exampletest/junit6/MethodsTest.java @@ -0,0 +1,30 @@ +package com.tngtech.archunit.exampletest.junit6; + +import com.tngtech.archunit.example.layers.anticorruption.WrappedResult; +import com.tngtech.archunit.example.layers.security.Secured; +import com.tngtech.archunit.junit.AnalyzeClasses; +import com.tngtech.archunit.junit.ArchTag; +import com.tngtech.archunit.junit.ArchTest; +import com.tngtech.archunit.lang.ArchRule; + +import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.methods; +import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noCodeUnits; + +@ArchTag("example") +@AnalyzeClasses(packages = "com.tngtech.archunit.example.layers") +public class MethodsTest { + + @ArchTest + static ArchRule all_public_methods_in_the_controller_layer_should_return_API_response_wrappers = + methods() + .that().areDeclaredInClassesThat().resideInAPackage("..anticorruption..") + .and().arePublic() + .should().haveRawReturnType(WrappedResult.class) + .because("we do not want to couple the client code directly to the return types of the encapsulated module"); + + @ArchTest + static ArchRule code_units_in_DAO_layer_should_not_be_Secured = + noCodeUnits() + .that().areDeclaredInClassesThat().resideInAPackage("..persistence..") + .should().beAnnotatedWith(Secured.class); +} \ No newline at end of file diff --git a/archunit-example/example-junit6/src/test/java/com/tngtech/archunit/exampletest/junit6/ModulesTest.java b/archunit-example/example-junit6/src/test/java/com/tngtech/archunit/exampletest/junit6/ModulesTest.java new file mode 100644 index 0000000000..72cc175954 --- /dev/null +++ b/archunit-example/example-junit6/src/test/java/com/tngtech/archunit/exampletest/junit6/ModulesTest.java @@ -0,0 +1,190 @@ +package com.tngtech.archunit.exampletest.junit6; + +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.function.Supplier; + +import com.tngtech.archunit.base.DescribedFunction; +import com.tngtech.archunit.base.DescribedPredicate; +import com.tngtech.archunit.core.domain.JavaClass; +import com.tngtech.archunit.core.domain.JavaPackage; +import com.tngtech.archunit.example.AppModule; +import com.tngtech.archunit.example.ModuleApi; +import com.tngtech.archunit.junit.AnalyzeClasses; +import com.tngtech.archunit.junit.ArchTag; +import com.tngtech.archunit.junit.ArchTest; +import com.tngtech.archunit.lang.ArchRule; +import com.tngtech.archunit.library.modules.AnnotationDescriptor; +import com.tngtech.archunit.library.modules.ArchModule; +import com.tngtech.archunit.library.modules.ModuleDependency; +import com.tngtech.archunit.library.modules.syntax.DescriptorFunction; + +import static com.tngtech.archunit.base.DescribedPredicate.alwaysTrue; +import static com.tngtech.archunit.core.domain.JavaClass.Predicates.belongToAnyOf; +import static com.tngtech.archunit.library.modules.syntax.AllowedModuleDependencies.allow; +import static com.tngtech.archunit.library.modules.syntax.ModuleDependencyScope.consideringOnlyDependenciesInAnyPackage; +import static com.tngtech.archunit.library.modules.syntax.ModuleRuleDefinition.modules; +import static java.util.Arrays.stream; +import static java.util.stream.Collectors.toList; + +@ArchTag("example") +@AnalyzeClasses(packages = "com.tngtech.archunit.example") +public class ModulesTest { + + /** + * This example demonstrates how to derive modules from a package pattern. + * The `..` stands for arbitrary many packages and the `(*)` captures one specific subpackage name within the + * package tree. + */ + @ArchTest + static ArchRule modules_should_respect_their_declared_dependencies__use_package_API = + modules() + .definedByPackages("..shopping.(*)..") + .should().respectTheirAllowedDependencies( + allow() + .fromModule("catalog").toModules("product") + .fromModule("customer").toModules("address") + .fromModule("importer").toModules("catalog", "xml") + .fromModule("order").toModules("customer", "product"), + consideringOnlyDependenciesInAnyPackage("..example..")) + .ignoreDependency(alwaysTrue(), belongToAnyOf(AppModule.class, ModuleApi.class)); + + /** + * This example demonstrates how to easily derive modules from classes annotated with a certain annotation, + * and also test for allowed dependencies and correct access to exposed packages by declared descriptor annotation properties. + * Within the example those are simply package-info files which denote the root of the modules by + * being annotated with @AppModule. + */ + @ArchTest + static ArchRule modules_should_respect_their_declared_dependencies_and_exposed_packages = + modules() + .definedByAnnotation(AppModule.class) + .should().respectTheirAllowedDependenciesDeclaredIn("allowedDependencies", + consideringOnlyDependenciesInAnyPackage("..example..")) + .ignoreDependency(alwaysTrue(), belongToAnyOf(AppModule.class, ModuleApi.class)) + .andShould().onlyDependOnEachOtherThroughPackagesDeclaredIn("exposedPackages"); + + /** + * This example demonstrates how to easily derive modules from classes annotated with a certain annotation, + * and also test for allowed dependencies using the descriptor annotation. + * Within the example those are simply package-info files which denote the root of the modules by + * being annotated with @AppModule. + */ + @ArchTest + static ArchRule modules_should_respect_their_declared_dependencies__use_annotation_API = + modules() + .definedByAnnotation(AppModule.class) + .should().respectTheirAllowedDependencies( + declaredByDescriptorAnnotation(), + consideringOnlyDependenciesInAnyPackage("..example..") + ) + .ignoreDependency(alwaysTrue(), belongToAnyOf(AppModule.class, ModuleApi.class)); + + /** + * This example demonstrates how to use the slightly more generic root class API to define modules. + * While the result in this example is the same as the above, this API in general can be used to + * use arbitrary classes as roots of modules. + * For example if there is always a central interface denoted in some way, + * the modules could be derived from these interfaces. + */ + @ArchTest + static ArchRule modules_should_respect_their_declared_dependencies__use_root_class_API = + modules() + .definedByRootClasses( + DescribedPredicate.describe("annotated with @" + AppModule.class.getSimpleName(), (JavaClass rootClass) -> + rootClass.isAnnotatedWith(AppModule.class)) + ) + .derivingModuleFromRootClassBy( + DescribedFunction.describe("annotation @" + AppModule.class.getSimpleName(), (JavaClass rootClass) -> { + AppModule module = rootClass.getAnnotationOfType(AppModule.class); + return new AnnotationDescriptor<>(module.name(), module); + }) + ) + .should().respectTheirAllowedDependencies( + declaredByDescriptorAnnotation(), + consideringOnlyDependenciesInAnyPackage("..example..") + ) + .ignoreDependency(alwaysTrue(), belongToAnyOf(AppModule.class, ModuleApi.class)); + + /** + * This example demonstrates how to use the generic API to define modules. + * The result in this example again is the same as the above, however in general the generic API + * allows to derive modules in a completely customizable way. + */ + @ArchTest + static ArchRule modules_should_respect_their_declared_dependencies__use_generic_API = + modules() + .definedBy(identifierFromModulesAnnotation()) + .derivingModule(fromModulesAnnotation()) + .should().respectTheirAllowedDependencies( + declaredByDescriptorAnnotation(), + consideringOnlyDependenciesInAnyPackage("..example..") + ) + .ignoreDependency(alwaysTrue(), belongToAnyOf(AppModule.class, ModuleApi.class)); + + /** + * This example demonstrates how to check that modules only depend on each other through a specific API. + */ + @ArchTest + static ArchRule modules_should_only_depend_on_each_other_through_module_API = + modules() + .definedByAnnotation(AppModule.class) + .should().onlyDependOnEachOtherThroughClassesThat().areAnnotatedWith(ModuleApi.class); + + /** + * This example demonstrates how to check for cyclic dependencies between modules. + */ + @ArchTest + static ArchRule modules_should_be_free_of_cycles = + modules() + .definedByAnnotation(AppModule.class) + .should().beFreeOfCycles(); + + private static DescribedPredicate>> declaredByDescriptorAnnotation() { + return DescribedPredicate.describe("declared by descriptor annotation", moduleDependency -> { + AppModule descriptor = moduleDependency.getOrigin().getDescriptor().getAnnotation(); + List allowedDependencies = stream(descriptor.allowedDependencies()).collect(toList()); + return allowedDependencies.contains(moduleDependency.getTarget().getName()); + }); + } + + private static IdentifierFromAnnotation identifierFromModulesAnnotation() { + return new IdentifierFromAnnotation(); + } + + private static DescriptorFunction> fromModulesAnnotation() { + return DescriptorFunction.describe(String.format("from @%s(name)", AppModule.class.getSimpleName()), + (ArchModule.Identifier identifier, Set containedClasses) -> { + JavaClass rootClass = containedClasses.stream().filter(it -> it.isAnnotatedWith(AppModule.class)).findFirst().get(); + AppModule module = rootClass.getAnnotationOfType(AppModule.class); + return new AnnotationDescriptor<>(module.name(), module); + }); + } + + private static class IdentifierFromAnnotation extends DescribedFunction { + IdentifierFromAnnotation() { + super("root classes with annotation @" + AppModule.class.getSimpleName()); + } + + @Override + public ArchModule.Identifier apply(JavaClass javaClass) { + return getIdentifierOfPackage(javaClass.getPackage()); + } + + private ArchModule.Identifier getIdentifierOfPackage(JavaPackage javaPackage) { + Optional identifierInCurrentPackage = javaPackage.getClasses().stream() + .filter(it -> it.isAnnotatedWith(AppModule.class)) + .findFirst() + .map(annotatedClassInPackage -> ArchModule.Identifier.from(annotatedClassInPackage.getAnnotationOfType(AppModule.class).name())); + + return identifierInCurrentPackage.orElseGet(identifierInParentPackageOf(javaPackage)); + } + + private Supplier identifierInParentPackageOf(JavaPackage javaPackage) { + return () -> javaPackage.getParent() + .map(this::getIdentifierOfPackage) + .orElseGet(ArchModule.Identifier::ignore); + } + } +} diff --git a/archunit-example/example-junit6/src/test/java/com/tngtech/archunit/exampletest/junit6/NamingConventionTest.java b/archunit-example/example-junit6/src/test/java/com/tngtech/archunit/exampletest/junit6/NamingConventionTest.java new file mode 100644 index 0000000000..e6303c7b9e --- /dev/null +++ b/archunit-example/example-junit6/src/test/java/com/tngtech/archunit/exampletest/junit6/NamingConventionTest.java @@ -0,0 +1,44 @@ +package com.tngtech.archunit.exampletest.junit6; + +import com.tngtech.archunit.example.layers.AbstractController; +import com.tngtech.archunit.example.layers.MyController; +import com.tngtech.archunit.example.layers.MyService; +import com.tngtech.archunit.junit.AnalyzeClasses; +import com.tngtech.archunit.junit.ArchTag; +import com.tngtech.archunit.junit.ArchTest; +import com.tngtech.archunit.lang.ArchRule; + +import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes; + +@ArchTag("example") +@AnalyzeClasses(packages = "com.tngtech.archunit.example.layers") +public class NamingConventionTest { + + @ArchTest + static ArchRule services_should_be_prefixed = + classes() + .that().resideInAPackage("..service..") + .and().areAnnotatedWith(MyService.class) + .should().haveSimpleNameStartingWith("Service"); + + @ArchTest + static ArchRule controllers_should_not_have_Gui_in_name = + classes() + .that().resideInAPackage("..controller..") + .should().haveSimpleNameNotContaining("Gui"); + + @ArchTest + static ArchRule controllers_should_be_suffixed = + classes() + .that().resideInAPackage("..controller..") + .or().areAnnotatedWith(MyController.class) + .or().areAssignableTo(AbstractController.class) + .should().haveSimpleNameEndingWith("Controller"); + + @ArchTest + static ArchRule classes_named_controller_should_be_in_a_controller_package = + classes() + .that().haveSimpleNameContaining("Controller") + .should().resideInAPackage("..controller.."); + +} diff --git a/archunit-example/example-junit6/src/test/java/com/tngtech/archunit/exampletest/junit6/OnionArchitectureTest.java b/archunit-example/example-junit6/src/test/java/com/tngtech/archunit/exampletest/junit6/OnionArchitectureTest.java new file mode 100644 index 0000000000..7aa16eece0 --- /dev/null +++ b/archunit-example/example-junit6/src/test/java/com/tngtech/archunit/exampletest/junit6/OnionArchitectureTest.java @@ -0,0 +1,77 @@ +package com.tngtech.archunit.exampletest.junit6; + +import java.lang.annotation.Annotation; + +import com.tngtech.archunit.base.DescribedPredicate; +import com.tngtech.archunit.core.domain.JavaAnnotation; +import com.tngtech.archunit.core.domain.JavaClass; +import com.tngtech.archunit.core.domain.properties.CanBeAnnotated; +import com.tngtech.archunit.example.onionarchitecture.domain.model.OrderItem; +import com.tngtech.archunit.example.onionarchitecture.domain.service.OrderQuantity; +import com.tngtech.archunit.example.onionarchitecture_by_annotations.annotations.Adapter; +import com.tngtech.archunit.example.onionarchitecture_by_annotations.annotations.Application; +import com.tngtech.archunit.example.onionarchitecture_by_annotations.annotations.DomainModel; +import com.tngtech.archunit.example.onionarchitecture_by_annotations.annotations.DomainService; +import com.tngtech.archunit.junit.AnalyzeClasses; +import com.tngtech.archunit.junit.ArchTag; +import com.tngtech.archunit.junit.ArchTest; +import com.tngtech.archunit.lang.ArchRule; + +import static com.tngtech.archunit.base.DescribedPredicate.describe; +import static com.tngtech.archunit.core.domain.JavaClass.Predicates.belongTo; +import static com.tngtech.archunit.core.domain.properties.CanBeAnnotated.Predicates.annotatedWith; +import static com.tngtech.archunit.library.Architectures.onionArchitecture; + +@ArchTag("example") +@AnalyzeClasses(packages = { + "com.tngtech.archunit.example.onionarchitecture", + "com.tngtech.archunit.example.onionarchitecture_by_annotations" +}) +public class OnionArchitectureTest { + + @ArchTest + static final ArchRule onion_architecture_is_respected = onionArchitecture() + .domainModels("..domain.model..") + .domainServices("..domain.service..") + .applicationServices("..application..") + .adapter("cli", "..adapter.cli..") + .adapter("persistence", "..adapter.persistence..") + .adapter("rest", "..adapter.rest.."); + + @ArchTest + static final ArchRule onion_architecture_is_respected_with_exception = onionArchitecture() + .domainModels("..domain.model..") + .domainServices("..domain.service..") + .applicationServices("..application..") + .adapter("cli", "..adapter.cli..") + .adapter("persistence", "..adapter.persistence..") + .adapter("rest", "..adapter.rest..") + + .ignoreDependency(OrderItem.class, OrderQuantity.class); + + @ArchTest + static final ArchRule onion_architecture_defined_by_annotations = onionArchitecture() + .domainModels(byAnnotation(DomainModel.class)) + .domainServices(byAnnotation(DomainService.class)) + .applicationServices(byAnnotation(Application.class)) + .adapter("cli", byAnnotation(adapter("cli"))) + .adapter("persistence", byAnnotation(adapter("persistence"))) + .adapter("rest", byAnnotation(adapter("rest"))); + + private static DescribedPredicate byAnnotation(Class annotationType) { + DescribedPredicate annotatedWith = annotatedWith(annotationType); + return belongTo(annotatedWith).as(annotatedWith.getDescription()); + } + + private static DescribedPredicate byAnnotation(DescribedPredicate> annotationType) { + DescribedPredicate annotatedWith = annotatedWith(annotationType); + return belongTo(annotatedWith).as(annotatedWith.getDescription()); + } + + private static DescribedPredicate> adapter(String adapterName) { + return describe( + String.format("@%s(\"%s\")", Adapter.class.getSimpleName(), adapterName), + a -> a.getRawType().isEquivalentTo(Adapter.class) && a.as(Adapter.class).value().equals(adapterName) + ); + } +} diff --git a/archunit-example/example-junit6/src/test/java/com/tngtech/archunit/exampletest/junit6/PlantUmlArchitectureTest.java b/archunit-example/example-junit6/src/test/java/com/tngtech/archunit/exampletest/junit6/PlantUmlArchitectureTest.java new file mode 100644 index 0000000000..0f5720446b --- /dev/null +++ b/archunit-example/example-junit6/src/test/java/com/tngtech/archunit/exampletest/junit6/PlantUmlArchitectureTest.java @@ -0,0 +1,44 @@ +package com.tngtech.archunit.exampletest.junit6; + +import java.net.URL; + +import com.tngtech.archunit.core.domain.PackageMatchers; +import com.tngtech.archunit.example.shopping.catalog.ProductCatalog; +import com.tngtech.archunit.example.shopping.order.Order; +import com.tngtech.archunit.example.shopping.product.Product; +import com.tngtech.archunit.junit.AnalyzeClasses; +import com.tngtech.archunit.junit.ArchTag; +import com.tngtech.archunit.junit.ArchTest; +import com.tngtech.archunit.lang.ArchRule; + +import static com.tngtech.archunit.core.domain.Dependency.Predicates.dependency; +import static com.tngtech.archunit.core.domain.JavaClass.Functions.GET_PACKAGE_NAME; +import static com.tngtech.archunit.core.domain.JavaClass.Predicates.equivalentTo; +import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes; +import static com.tngtech.archunit.library.plantuml.rules.PlantUmlArchCondition.Configuration.consideringAllDependencies; +import static com.tngtech.archunit.library.plantuml.rules.PlantUmlArchCondition.Configuration.consideringOnlyDependenciesInAnyPackage; +import static com.tngtech.archunit.library.plantuml.rules.PlantUmlArchCondition.Configuration.consideringOnlyDependenciesInDiagram; +import static com.tngtech.archunit.library.plantuml.rules.PlantUmlArchCondition.adhereToPlantUmlDiagram; + +@ArchTag("example") +@AnalyzeClasses(packages = "com.tngtech.archunit.example.shopping") +public class PlantUmlArchitectureTest { + private static final URL plantUmlDiagram = PlantUmlArchitectureTest.class.getResource("shopping_example.puml"); + + @ArchTest + static final ArchRule classes_should_adhere_to_shopping_example_considering_only_dependencies_in_diagram = + classes().should(adhereToPlantUmlDiagram(plantUmlDiagram, consideringOnlyDependenciesInDiagram())); + + @ArchTest + static final ArchRule classes_should_adhere_to_shopping_example_considering_all_dependencies_and_ignoring_some_dependencies = + classes().should(adhereToPlantUmlDiagram(plantUmlDiagram, consideringAllDependencies()) + .ignoreDependenciesWithOrigin(equivalentTo(ProductCatalog.class)) + .ignoreDependenciesWithTarget(GET_PACKAGE_NAME.is(PackageMatchers.of("", "java..")).as("that is part of JDK")) + .ignoreDependencies(dependency(Product.class, Order.class) + .as(String.format("ignoring dependencies from %s to %s", Product.class.getName(), Order.class.getName())))); + + @ArchTest + static final ArchRule classes_should_adhere_to_shopping_example_considering_only_dependencies_in_any_package = + classes().should(adhereToPlantUmlDiagram(plantUmlDiagram, + consideringOnlyDependenciesInAnyPackage("..catalog"))); +} diff --git a/archunit-example/example-junit6/src/test/java/com/tngtech/archunit/exampletest/junit6/ProxyRulesTest.java b/archunit-example/example-junit6/src/test/java/com/tngtech/archunit/exampletest/junit6/ProxyRulesTest.java new file mode 100644 index 0000000000..a6a0409955 --- /dev/null +++ b/archunit-example/example-junit6/src/test/java/com/tngtech/archunit/exampletest/junit6/ProxyRulesTest.java @@ -0,0 +1,20 @@ +package com.tngtech.archunit.exampletest.junit6; + +import com.tngtech.archunit.example.layers.ClassViolatingCodingRules; +import com.tngtech.archunit.example.layers.service.Async; +import com.tngtech.archunit.junit.AnalyzeClasses; +import com.tngtech.archunit.junit.ArchTag; +import com.tngtech.archunit.junit.ArchTest; +import com.tngtech.archunit.lang.ArchRule; + +import static com.tngtech.archunit.library.ProxyRules.no_classes_should_directly_call_other_methods_declared_in_the_same_class_that_are_annotated_with; + +@ArchTag("example") +@AnalyzeClasses(packagesOf = ClassViolatingCodingRules.class) +public class ProxyRulesTest { + + @ArchTest + static ArchRule no_bypass_of_proxy_logic = + no_classes_should_directly_call_other_methods_declared_in_the_same_class_that_are_annotated_with(Async.class); + +} diff --git a/archunit-example/example-junit6/src/test/java/com/tngtech/archunit/exampletest/junit6/RestrictNumberOfClassesWithACertainPropertyTest.java b/archunit-example/example-junit6/src/test/java/com/tngtech/archunit/exampletest/junit6/RestrictNumberOfClassesWithACertainPropertyTest.java new file mode 100644 index 0000000000..9a63b1603f --- /dev/null +++ b/archunit-example/example-junit6/src/test/java/com/tngtech/archunit/exampletest/junit6/RestrictNumberOfClassesWithACertainPropertyTest.java @@ -0,0 +1,23 @@ +package com.tngtech.archunit.exampletest.junit6; + +import com.tngtech.archunit.example.layers.SomeBusinessInterface; +import com.tngtech.archunit.example.layers.SomeOtherBusinessInterface; +import com.tngtech.archunit.junit.AnalyzeClasses; +import com.tngtech.archunit.junit.ArchTag; +import com.tngtech.archunit.junit.ArchTest; +import com.tngtech.archunit.lang.ArchRule; + +import static com.tngtech.archunit.base.DescribedPredicate.lessThanOrEqualTo; +import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes; + +@ArchTag("example") +@AnalyzeClasses(packages = "com.tngtech.archunit.example.layers") +public class RestrictNumberOfClassesWithACertainPropertyTest { + + @ArchTest + static final ArchRule no_new_classes_should_implement_SomeBusinessInterface = + classes().that().implement(SomeBusinessInterface.class) + .should().containNumberOfElements(lessThanOrEqualTo(1)) + .because("from now on new classes should implement " + SomeOtherBusinessInterface.class.getName()); + +} diff --git a/archunit-example/example-junit6/src/test/java/com/tngtech/archunit/exampletest/junit6/RuleLibraryTest.java b/archunit-example/example-junit6/src/test/java/com/tngtech/archunit/exampletest/junit6/RuleLibraryTest.java new file mode 100644 index 0000000000..7d81246231 --- /dev/null +++ b/archunit-example/example-junit6/src/test/java/com/tngtech/archunit/exampletest/junit6/RuleLibraryTest.java @@ -0,0 +1,19 @@ +package com.tngtech.archunit.exampletest.junit6; + +import com.tngtech.archunit.junit.AnalyzeClasses; +import com.tngtech.archunit.junit.ArchTag; +import com.tngtech.archunit.junit.ArchTest; +import com.tngtech.archunit.junit.ArchTests; + +@ArchTag("example") +@AnalyzeClasses(packages = "com.tngtech.archunit.example.layers") +class RuleLibraryTest { + @ArchTest + static final ArchTests LIBRARY = ArchTests.in(RuleSetsTest.class); + + @ArchTest + static final ArchTests FURTHER_CODING_RULES = ArchTests.in(CodingRulesTest.class); + + @ArchTest + static final ArchTests SLICES_ISOLATION_RULES = ArchTests.in(SlicesIsolationTest.class); +} diff --git a/archunit-example/example-junit6/src/test/java/com/tngtech/archunit/exampletest/junit6/RuleSetsTest.java b/archunit-example/example-junit6/src/test/java/com/tngtech/archunit/exampletest/junit6/RuleSetsTest.java new file mode 100644 index 0000000000..6fb56140ff --- /dev/null +++ b/archunit-example/example-junit6/src/test/java/com/tngtech/archunit/exampletest/junit6/RuleSetsTest.java @@ -0,0 +1,16 @@ +package com.tngtech.archunit.exampletest.junit6; + +import com.tngtech.archunit.junit.AnalyzeClasses; +import com.tngtech.archunit.junit.ArchTag; +import com.tngtech.archunit.junit.ArchTest; +import com.tngtech.archunit.junit.ArchTests; + +@ArchTag("example") +@AnalyzeClasses(packages = "com.tngtech.archunit.example.layers") +class RuleSetsTest { + @ArchTest + private final ArchTests CODING_RULES = ArchTests.in(CodingRulesTest.class); + + @ArchTest + private final ArchTests NAMING_CONVENTION_RULES = ArchTests.in(NamingConventionTest.class); +} diff --git a/archunit-example/example-junit6/src/test/java/com/tngtech/archunit/exampletest/junit6/SessionBeanRulesTest.java b/archunit-example/example-junit6/src/test/java/com/tngtech/archunit/exampletest/junit6/SessionBeanRulesTest.java new file mode 100644 index 0000000000..f60f7bfb01 --- /dev/null +++ b/archunit-example/example-junit6/src/test/java/com/tngtech/archunit/exampletest/junit6/SessionBeanRulesTest.java @@ -0,0 +1,105 @@ +package com.tngtech.archunit.exampletest.junit6; + +import java.util.Set; + +import javax.ejb.Local; +import javax.ejb.Stateless; + +import com.tngtech.archunit.base.DescribedPredicate; +import com.tngtech.archunit.core.domain.AccessTarget.FieldAccessTarget; +import com.tngtech.archunit.core.domain.JavaAccess; +import com.tngtech.archunit.core.domain.JavaAccess.Functions.Get; +import com.tngtech.archunit.core.domain.JavaClass; +import com.tngtech.archunit.core.domain.JavaFieldAccess; +import com.tngtech.archunit.core.domain.properties.HasOwner; +import com.tngtech.archunit.junit.AnalyzeClasses; +import com.tngtech.archunit.junit.ArchTag; +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.lang.SimpleConditionEvent; + +import static com.tngtech.archunit.base.DescribedPredicate.not; +import static com.tngtech.archunit.core.domain.JavaAccess.Predicates.originOwnerEqualsTargetOwner; +import static com.tngtech.archunit.core.domain.JavaClass.Predicates.INTERFACES; +import static com.tngtech.archunit.core.domain.JavaCodeUnit.Predicates.constructor; +import static com.tngtech.archunit.core.domain.properties.CanBeAnnotated.Predicates.annotatedWith; +import static com.tngtech.archunit.lang.conditions.ArchPredicates.are; +import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes; +import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses; +import static java.util.stream.Collectors.joining; + +@ArchTag("example") +@AnalyzeClasses(packages = "com.tngtech.archunit.example.layers") +public class SessionBeanRulesTest { + + @ArchTest + static final ArchRule stateless_session_beans_should_not_have_state = + noClasses().should() + .setFieldWhere(targetIsStatelessSessionBean().and(accessOriginIsOutsideOfConstruction())) + .as("No Stateless Session Bean should have state"); + + @ArchTest + static final ArchRule business_interface_implementations_should_be_unique = + classes().that(are(businessInterfaces())).should(haveAUniqueImplementation()); + + private static DescribedPredicate targetIsStatelessSessionBean() { + return Get.target() + .then(HasOwner.Functions.Get.owner()) + .is(annotatedWith(Stateless.class)); + } + + private static DescribedPredicate> accessOriginIsOutsideOfConstruction() { + return not(originOwnerEqualsTargetOwner()).or(originNeitherConstructorNorPostConstruct()); + } + + private static DescribedPredicate> originNeitherConstructorNorPostConstruct() { + return Get.origin().is(not(constructor()).and(not(annotatedWith("javax.annotation.PostConstruct")))); + } + + private static DescribedPredicate haveLocalBeanSubclass() { + return new DescribedPredicate("have subclass that is a local bean") { + @Override + public boolean test(JavaClass input) { + return input.getAllSubclasses().stream().anyMatch(subclass -> isLocalBeanImplementation(subclass, input)); + } + + // NOTE: We assume that in this project by convention @Local is always used as @Local(type) with exactly + // one type, otherwise this would need to be more sophisticated + private boolean isLocalBeanImplementation(JavaClass bean, JavaClass businessInterfaceType) { + return bean.isAnnotatedWith(Local.class) && + bean.getAnnotationOfType(Local.class).value()[0].getName() + .equals(businessInterfaceType.getName()); + } + }; + } + + private static DescribedPredicate businessInterfaces() { + return INTERFACES.and(haveLocalBeanSubclass()).as("business interfaces"); + } + + private static ArchCondition haveAUniqueImplementation() { + return new ArchCondition("have a unique implementation") { + @Override + public void check(JavaClass businessInterface, ConditionEvents events) { + events.add(new SimpleConditionEvent(businessInterface, + businessInterface.getAllSubclasses().size() <= 1, + describe(businessInterface))); + } + + private String describe(JavaClass businessInterface) { + return String.format("%s is implemented by %s", + businessInterface.getSimpleName(), joinNamesOf(businessInterface.getAllSubclasses())); + } + + private String joinNamesOf(Set implementations) { + if (implementations.isEmpty()) { + return ""; + } + + return implementations.stream().map(JavaClass::getSimpleName).collect(joining(", ")); + } + }; + } +} diff --git a/archunit-example/example-junit6/src/test/java/com/tngtech/archunit/exampletest/junit6/SingleClassTest.java b/archunit-example/example-junit6/src/test/java/com/tngtech/archunit/exampletest/junit6/SingleClassTest.java new file mode 100644 index 0000000000..edcdfc34ed --- /dev/null +++ b/archunit-example/example-junit6/src/test/java/com/tngtech/archunit/exampletest/junit6/SingleClassTest.java @@ -0,0 +1,42 @@ +package com.tngtech.archunit.exampletest.junit6; + +import com.tngtech.archunit.example.layers.SomeOtherBusinessInterface; +import com.tngtech.archunit.example.layers.core.CoreSatellite; +import com.tngtech.archunit.example.layers.core.HighSecurity; +import com.tngtech.archunit.example.layers.core.VeryCentralCore; +import com.tngtech.archunit.junit.AnalyzeClasses; +import com.tngtech.archunit.junit.ArchTag; +import com.tngtech.archunit.junit.ArchTest; +import com.tngtech.archunit.lang.ArchRule; + +import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes; +import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClass; +import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.theClass; + +@ArchTag("example") +@AnalyzeClasses(packages = "com.tngtech.archunit.example.layers") +public class SingleClassTest { + + @ArchTest + static final ArchRule core_should_only_be_accessed_by_satellites = + theClass(VeryCentralCore.class) + .should().onlyBeAccessed().byClassesThat().implement(CoreSatellite.class); + + @ArchTest + static final ArchRule core_should_only_access_classes_in_core_itself = + noClass(VeryCentralCore.class) + .should().accessClassesThat().resideOutsideOfPackages("..core..", "java.."); + + @ArchTest + static final ArchRule the_only_class_with_high_security_is_central_core = + classes() + .that().areAnnotatedWith(HighSecurity.class) + .should().be(VeryCentralCore.class); + + @ArchTest + static final ArchRule central_core_should_not_implement_some_business_interface = + classes() + .that().implement(SomeOtherBusinessInterface.class) + .should().notBe(VeryCentralCore.class); + +} diff --git a/archunit-example/example-junit6/src/test/java/com/tngtech/archunit/exampletest/junit6/SlicesIsolationTest.java b/archunit-example/example-junit6/src/test/java/com/tngtech/archunit/exampletest/junit6/SlicesIsolationTest.java new file mode 100644 index 0000000000..dc512a2d2b --- /dev/null +++ b/archunit-example/example-junit6/src/test/java/com/tngtech/archunit/exampletest/junit6/SlicesIsolationTest.java @@ -0,0 +1,46 @@ +package com.tngtech.archunit.exampletest.junit6; + +import com.tngtech.archunit.base.DescribedPredicate; +import com.tngtech.archunit.example.layers.controller.one.UseCaseOneTwoController; +import com.tngtech.archunit.example.layers.controller.two.UseCaseTwoController; +import com.tngtech.archunit.junit.AnalyzeClasses; +import com.tngtech.archunit.junit.ArchTag; +import com.tngtech.archunit.junit.ArchTest; +import com.tngtech.archunit.lang.ArchRule; +import com.tngtech.archunit.library.dependencies.Slice; + +import static com.tngtech.archunit.base.DescribedPredicate.alwaysTrue; +import static com.tngtech.archunit.core.domain.properties.HasName.Predicates.nameMatching; +import static com.tngtech.archunit.library.dependencies.SlicesRuleDefinition.slices; + +@ArchTag("example") +@AnalyzeClasses(packages = "com.tngtech.archunit.example.layers") +public class SlicesIsolationTest { + @ArchTest + static final ArchRule controllers_should_only_use_their_own_slice = + slices().matching("..controller.(*)..").namingSlices("Controller $1") + .as("Controllers").should().notDependOnEachOther(); + + @ArchTest + static final ArchRule specific_controllers_should_only_use_their_own_slice = + slices().matching("..controller.(*)..").namingSlices("Controller $1") + .that(containDescription("Controller one")) + .or(containDescription("Controller two")) + .as("Controllers one and two").should().notDependOnEachOther(); + + @ArchTest + static final ArchRule controllers_should_only_use_their_own_slice_with_custom_ignore = + slices().matching("..controller.(*)..").namingSlices("Controller $1") + .as("Controllers").should().notDependOnEachOther() + .ignoreDependency(UseCaseOneTwoController.class, UseCaseTwoController.class) + .ignoreDependency(nameMatching(".*controller\\.three.*"), alwaysTrue()); + + private static DescribedPredicate containDescription(String descriptionPart) { + return new DescribedPredicate("contain description '%s'", descriptionPart) { + @Override + public boolean test(Slice input) { + return input.getDescription().contains(descriptionPart); + } + }; + } +} diff --git a/archunit-example/example-junit6/src/test/java/com/tngtech/archunit/exampletest/junit6/ThirdPartyRulesTest.java b/archunit-example/example-junit6/src/test/java/com/tngtech/archunit/exampletest/junit6/ThirdPartyRulesTest.java new file mode 100644 index 0000000000..7e41915692 --- /dev/null +++ b/archunit-example/example-junit6/src/test/java/com/tngtech/archunit/exampletest/junit6/ThirdPartyRulesTest.java @@ -0,0 +1,54 @@ +package com.tngtech.archunit.exampletest.junit6; + +import com.tngtech.archunit.base.DescribedPredicate; +import com.tngtech.archunit.core.domain.JavaCall; +import com.tngtech.archunit.core.domain.JavaClass; +import com.tngtech.archunit.example.layers.thirdparty.ThirdPartyClassWithProblem; +import com.tngtech.archunit.example.layers.thirdparty.ThirdPartyClassWorkaroundFactory; +import com.tngtech.archunit.junit.AnalyzeClasses; +import com.tngtech.archunit.junit.ArchTag; +import com.tngtech.archunit.junit.ArchTest; +import com.tngtech.archunit.lang.ArchCondition; +import com.tngtech.archunit.lang.ArchRule; + +import static com.tngtech.archunit.base.DescribedPredicate.not; +import static com.tngtech.archunit.core.domain.AccessTarget.Predicates.constructor; +import static com.tngtech.archunit.core.domain.JavaAccess.Predicates.originOwner; +import static com.tngtech.archunit.core.domain.JavaAccess.Predicates.targetOwner; +import static com.tngtech.archunit.core.domain.JavaCall.Predicates.target; +import static com.tngtech.archunit.core.domain.JavaClass.Predicates.assignableTo; +import static com.tngtech.archunit.core.domain.JavaClass.Predicates.equivalentTo; +import static com.tngtech.archunit.lang.conditions.ArchConditions.callCodeUnitWhere; +import static com.tngtech.archunit.lang.conditions.ArchConditions.never; +import static com.tngtech.archunit.lang.conditions.ArchPredicates.is; +import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes; + +@ArchTag("example") +@AnalyzeClasses(packages = "com.tngtech.archunit.example.layers") +public class ThirdPartyRulesTest { + + @ArchTest + static final ArchRule third_party_class_should_only_be_instantiated_via_workaround = + classes().should(notCreateProblematicClassesOutsideOfWorkaroundFactory() + .as("not instantiate %s and its subclasses, but instead use %s", + ThirdPartyClassWithProblem.class.getSimpleName(), + ThirdPartyClassWorkaroundFactory.class.getSimpleName())); + + private static ArchCondition notCreateProblematicClassesOutsideOfWorkaroundFactory() { + DescribedPredicate> constructorCallOfThirdPartyClass = + target(is(constructor())).and(targetOwner(is(assignableTo(ThirdPartyClassWithProblem.class)))); + + DescribedPredicate> notFromWithinThirdPartyClass = + originOwner(is(not(assignableTo(ThirdPartyClassWithProblem.class)))).forSubtype(); + + DescribedPredicate> notFromWorkaroundFactory = + originOwner(is(not(equivalentTo(ThirdPartyClassWorkaroundFactory.class)))).forSubtype(); + + DescribedPredicate> targetIsIllegalConstructorOfThirdPartyClass = + constructorCallOfThirdPartyClass. + and(notFromWithinThirdPartyClass). + and(notFromWorkaroundFactory); + + return never(callCodeUnitWhere(targetIsIllegalConstructorOfThirdPartyClass)); + } +} diff --git a/archunit-example/example-junit6/src/test/resources/archunit.properties b/archunit-example/example-junit6/src/test/resources/archunit.properties new file mode 100644 index 0000000000..b89dcc3d06 --- /dev/null +++ b/archunit-example/example-junit6/src/test/resources/archunit.properties @@ -0,0 +1 @@ +freeze.store.default.path=src/test/resources/frozen \ No newline at end of file diff --git a/archunit-example/example-junit6/src/test/resources/com/tngtech/archunit/exampletest/junit6/shopping_example.puml b/archunit-example/example-junit6/src/test/resources/com/tngtech/archunit/exampletest/junit6/shopping_example.puml new file mode 100644 index 0000000000..bc51d691ed --- /dev/null +++ b/archunit-example/example-junit6/src/test/resources/com/tngtech/archunit/exampletest/junit6/shopping_example.puml @@ -0,0 +1,29 @@ +@startuml + +skinparam componentStyle uml2 +skinparam component { + BorderColor #grey + BackgroundColor #white +} + +[Address] <<..address..>> +[Customer] <<..customer..>> +[Order] <<..order..>> #white/PowderBlue +[Products] <<..product..>> #white/PowderBlue +[Product Catalog] <<..catalog..>> as catalog #PowderBlue +[Product Import] <<..importer..>> as import + +' Could be some random comment +[XML] <<..xml.processor..>> <<..xml.types..>> as xml + +[Order] ---> [Customer] : is placed by +[Order] --> [Products] + +[Customer] --> [Address] + +[Products] <--[#green]- catalog +import -left-> catalog : parse products +import --> xml +note top on link #lightgreen: is responsible for translating XML to java classes + +@enduml diff --git a/archunit-example/example-junit6/src/test/resources/frozen/a81a2b54-5a18-4145-b544-7a580aba0425 b/archunit-example/example-junit6/src/test/resources/frozen/a81a2b54-5a18-4145-b544-7a580aba0425 new file mode 100644 index 0000000000..7c6aceeae0 --- /dev/null +++ b/archunit-example/example-junit6/src/test/resources/frozen/a81a2b54-5a18-4145-b544-7a580aba0425 @@ -0,0 +1,27 @@ +Class implements interface in (DaoCallingService.java:0) +Class has annotation member of type in (ServiceViolatingLayerRules.java:0) +Class has annotation member of type in (ServiceViolatingLayerRules.java:0) +Class is annotated with in (ServiceViolatingLayerRules.java:0) +Class extends class in (SpecialServiceHelper.java:0) +Class implements interface in (ServiceImplementation.java:0) +Constructor (com.tngtech.archunit.example.layers.service.ServiceViolatingLayerRules)> has parameter of type in (SomeMediator.java:0) +Constructor ()> calls constructor ()> in (SpecialServiceHelper.java:9) +Field has type in (SomeMediator.java:0) +Field has type in (SomeController.java:0) +Field has type in (SomeController.java:0) +Field has type in (DaoCallingService.java:0) +Field has type <[Lcom.tngtech.archunit.example.layers.service.ServiceType;> in (ServiceType.java:0) +Field has type in (ServiceViolatingDaoRules.java:0) +Method calls method in (SomeMediator.java:15) +Method calls constructor ()> in (SomeGuiController.java:7) +Method calls constructor (java.lang.String)> in (SomeGuiController.java:8) +Method gets field in (SomeGuiController.java:10) +Method gets field in (SomeGuiController.java:11) +Method calls method in (DaoCallingService.java:14) +Method has return type in (ComplexServiceAnnotation.java:0) +Method has return type in (ComplexServiceAnnotation.java:0) +Method calls method <[Lcom.tngtech.archunit.example.layers.service.ServiceType;.clone()> in (ServiceType.java:3) +Method has return type <[Lcom.tngtech.archunit.example.layers.service.ServiceType;> in (ServiceType.java:0) +Method has return type <[Lcom.tngtech.archunit.example.layers.service.ServiceType;> in (ServiceType.java:0) +Method calls method in (ServiceViolatingDaoRules.java:27) +Method is annotated with in (ServiceViolatingProxyRules.java:0) \ No newline at end of file diff --git a/archunit-example/example-junit6/src/test/resources/frozen/e77ec262-4d5c-4a7b-b41f-362a71e5a1d8 b/archunit-example/example-junit6/src/test/resources/frozen/e77ec262-4d5c-4a7b-b41f-362a71e5a1d8 new file mode 100644 index 0000000000..8b8178ae7f --- /dev/null +++ b/archunit-example/example-junit6/src/test/resources/frozen/e77ec262-4d5c-4a7b-b41f-362a71e5a1d8 @@ -0,0 +1,9 @@ +Class implements interface in (ServiceViolatingDaoRules.java:0) +Field has type in (SomeJpa.java:0) +Field has type in (OtherJpa.java:0) +Field has type in (ServiceViolatingDaoRules.java:0) +Method calls method in (SomeJpa.java:20) +Method has return type in (OtherDao.java:0) +Method calls method in (OtherJpa.java:19) +Method has return type in (OtherJpa.java:0) +Method calls method in (OtherJpa.java:24) \ No newline at end of file diff --git a/archunit-example/example-junit6/src/test/resources/frozen/stored.rules b/archunit-example/example-junit6/src/test/resources/frozen/stored.rules new file mode 100644 index 0000000000..4119dd0c97 --- /dev/null +++ b/archunit-example/example-junit6/src/test/resources/frozen/stored.rules @@ -0,0 +1,4 @@ +# +#Sun Jun 09 00:30:33 ICT 2019 +no\ classes\ should\ depend\ on\ classes\ that\ reside\ in\ a\ package\ '..service..'=a81a2b54-5a18-4145-b544-7a580aba0425 +no\ classes\ should\ depend\ on\ classes\ that\ are\ assignable\ to\ javax.persistence.EntityManager=e77ec262-4d5c-4a7b-b41f-362a71e5a1d8 diff --git a/archunit-example/example-junit6/src/test/resources/log4j2.xml b/archunit-example/example-junit6/src/test/resources/log4j2.xml new file mode 100644 index 0000000000..35a6a3ccdf --- /dev/null +++ b/archunit-example/example-junit6/src/test/resources/log4j2.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/archunit-example/example-plain/build.gradle b/archunit-example/example-plain/build.gradle index 256e5319de..807c0474ca 100644 --- a/archunit-example/example-plain/build.gradle +++ b/archunit-example/example-plain/build.gradle @@ -1,5 +1,6 @@ plugins { id 'archunit.java-examples-conventions' + id 'archunit.self-test-conventions' } ext.moduleName = 'com.tngtech.archunit.example.plain' diff --git a/archunit-example/example-plain/src/archTest/java/com/tngtech/archunit/exampletest/ArchUnitArchitectureTest.java b/archunit-example/example-plain/src/archTest/java/com/tngtech/archunit/exampletest/ArchUnitArchitectureTest.java new file mode 100644 index 0000000000..550ea70408 --- /dev/null +++ b/archunit-example/example-plain/src/archTest/java/com/tngtech/archunit/exampletest/ArchUnitArchitectureTest.java @@ -0,0 +1,16 @@ +package com.tngtech.archunit.exampletest; + +import com.tngtech.archunit.ArchUnitExampleArchitectureRules; +import com.tngtech.archunit.junit.AnalyzeClasses; +import com.tngtech.archunit.junit.ArchTest; +import com.tngtech.archunit.junit.ArchTests; + +@AnalyzeClasses( + packages = {"com.tngtech.archunit.example", "com.tngtech.archunit.exampletest"}) +public class ArchUnitArchitectureTest { + private ArchUnitArchitectureTest() { + } + + @ArchTest + public static final ArchTests example_rules = ArchTests.in(ArchUnitExampleArchitectureRules.class); +} diff --git a/archunit-integration-test/junit4/src/test/java/com/tngtech/archunit/integration/ExamplesJUnit4IntegrationTest.java b/archunit-integration-test/junit4/src/test/java/com/tngtech/archunit/integration/ExamplesJUnit4IntegrationTest.java index bd5eba52d3..ac5685ed0f 100644 --- a/archunit-integration-test/junit4/src/test/java/com/tngtech/archunit/integration/ExamplesJUnit4IntegrationTest.java +++ b/archunit-integration-test/junit4/src/test/java/com/tngtech/archunit/integration/ExamplesJUnit4IntegrationTest.java @@ -74,6 +74,7 @@ Stream FrozenRulesTest() { return FrozenRulesTest(com.tngtech.archunit.exampletest.junit4.FrozenRulesTest.class, this::withTemporaryViolationStore); } + /** Without this step, older Java versions (e.g. Java 11) modify the ArchUnit store in the source code. */ private void withTemporaryViolationStore(Runnable test) { try { File sourceDir = Paths.get(ArchConfiguration.get().getProperty("freeze.store.default.path")).toFile(); diff --git a/archunit-integration-test/junit5/src/test/java/com/tngtech/archunit/integration/ExamplesJUnit5IntegrationTest.java b/archunit-integration-test/junit5/src/test/java/com/tngtech/archunit/integration/ExamplesJUnit5IntegrationTest.java index b01f358bf1..4a3505c262 100644 --- a/archunit-integration-test/junit5/src/test/java/com/tngtech/archunit/integration/ExamplesJUnit5IntegrationTest.java +++ b/archunit-integration-test/junit5/src/test/java/com/tngtech/archunit/integration/ExamplesJUnit5IntegrationTest.java @@ -74,6 +74,7 @@ Stream FrozenRulesTest() { return FrozenRulesTest(com.tngtech.archunit.exampletest.junit5.FrozenRulesTest.class, this::withTemporaryViolationStore); } + /** Without this step, older Java versions (e.g. Java 11) modify the ArchUnit store in the source code. */ private void withTemporaryViolationStore(Runnable test) { try { File sourceDir = Paths.get(ArchConfiguration.get().getProperty("freeze.store.default.path")).toFile(); diff --git a/archunit-integration-test/junit6/build.gradle b/archunit-integration-test/junit6/build.gradle new file mode 100644 index 0000000000..238e1e2c87 --- /dev/null +++ b/archunit-integration-test/junit6/build.gradle @@ -0,0 +1,24 @@ +plugins { + id 'archunit.java-conventions' +} + +ext.moduleName = 'com.tngtech.archunit.integrationtest.junit6' + +ext.minimumJavaVersion = JavaVersion.VERSION_17 + +dependencies { + testImplementation libs.junit6PlatformLauncher + testImplementation libs.junit6Jupiter + testImplementation libs.assertj + testImplementation project(path: ':archunit', configuration: 'tests') + testImplementation project(path: ':archunit-junit6-api') + testImplementation project(path: ':archunit-junit6-engine-api') + testImplementation project(path: ':archunit-integration-test:shared') + testImplementation project(path: ':archunit-example:example-plain') + testImplementation project(path: ':archunit-example:example-plain', configuration: 'tests') + testImplementation project(path: ':archunit-example:example-junit6', configuration: 'tests') + + testRuntimeOnly project(path: ':archunit-junit6-engine') + // TODO still up-to-date? + testRuntimeOnly libs.junit6VintageEngine // needed because `archunit-example:example-plain` uses JUnit 4 annotations +} diff --git a/archunit-integration-test/junit6/src/test/java/com/tngtech/archunit/integration/CustomClassLoaderIntegrationTest.java b/archunit-integration-test/junit6/src/test/java/com/tngtech/archunit/integration/CustomClassLoaderIntegrationTest.java new file mode 100644 index 0000000000..e163962568 --- /dev/null +++ b/archunit-integration-test/junit6/src/test/java/com/tngtech/archunit/integration/CustomClassLoaderIntegrationTest.java @@ -0,0 +1,112 @@ +package com.tngtech.archunit.integration; + +import java.lang.reflect.AccessibleObject; +import java.lang.reflect.Method; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import com.google.common.collect.ImmutableList; +import com.tngtech.archunit.core.importer.UrlSourceTestUtils; +import com.tngtech.archunit.exampletest.junit6.CodingRulesTest; +import com.tngtech.archunit.testutils.ContextClassLoaderExtension; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.platform.engine.TestExecutionResult; +import org.junit.platform.launcher.Launcher; +import org.junit.platform.launcher.TestExecutionListener; +import org.junit.platform.launcher.TestIdentifier; +import org.junit.platform.launcher.core.LauncherFactory; + +import static com.google.common.collect.Iterables.getOnlyElement; +import static com.tngtech.archunit.junit.engine_api.FieldSelector.selectField; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder.request; + +@ExtendWith(ContextClassLoaderExtension.class) +class CustomClassLoaderIntegrationTest { + + @Test + void should_load_test_classes_with_context_ClassLoader() throws Exception { + // This is a simplified version of the setup that e.g. Quarkus continuous testing is using. + // The ClassLoader that loads the test infrastructure does not know the concrete test classes, only the context ClassLoader does + + CustomTestClassLoader customTestClassLoader = new CustomTestClassLoader(); + Thread.currentThread().setContextClassLoader(customTestClassLoader.testClassLoader); + + JUnitPlatformLauncherFromOtherClassLoader launcher = new JUnitPlatformLauncherFromOtherClassLoader(customTestClassLoader); + + List testFailureMessage = launcher.run(CodingRulesTest.class, "no_access_to_standard_streams"); + + assertThat(getOnlyElement(testFailureMessage)).contains("Rule 'no classes should access standard streams' was violated"); + } + + private static class CustomTestClassLoader { + private final URLClassLoader testInfrastructureClassLoader; + private final URLClassLoader testClassLoader; + + public CustomTestClassLoader() { + URL[] fullClasspath = ImmutableList.copyOf(UrlSourceTestUtils.getClasspath()).toArray(new URL[0]); + URL[] testInfrastructureClasspath = Arrays.stream(fullClasspath) + .filter(url -> !url.toString().contains("example")) + .toArray(URL[]::new); + testInfrastructureClassLoader = new URLClassLoader(testInfrastructureClasspath, null); + testClassLoader = new URLClassLoader(fullClasspath, testInfrastructureClassLoader); + } + + Class loadTestInfrastructure(Class clazz) throws ClassNotFoundException { + return testInfrastructureClassLoader.loadClass(clazz.getName()); + } + + Class loadTestClass(Class clazz) throws ClassNotFoundException { + return testClassLoader.loadClass(clazz.getName()); + } + } + + private static class JUnitPlatformLauncherFromOtherClassLoader { + private final CustomTestClassLoader customTestClassLoader; + + JUnitPlatformLauncherFromOtherClassLoader(CustomTestClassLoader customTestClassLoader) { + this.customTestClassLoader = customTestClassLoader; + } + + public List run(Class testClass, String ruleName) throws Exception { + Class launcherExecutorClassFromOtherClassLoader = customTestClassLoader.loadTestInfrastructure(JUnitPlatformLauncherExecutor.class); + Class testClassFromOtherClassLoader = customTestClassLoader.loadTestClass(testClass); + Object launcherExecutor = accessible(launcherExecutorClassFromOtherClassLoader.getDeclaredConstructor()).newInstance(); + + return invoke(launcherExecutor, "run", testClassFromOtherClassLoader, ruleName); + } + + @SuppressWarnings({"unchecked", "SameParameterValue"}) // Access through Reflection can never be typesafe, the caller needs to decide + private T invoke(Object owner, String methodName, Object... params) throws Exception { + Class[] parameterTypes = Arrays.stream(params).map(Object::getClass).toArray(Class[]::new); + Method runMethod = owner.getClass().getDeclaredMethod(methodName, parameterTypes); + return (T) accessible(runMethod).invoke(owner, params); + } + + private T accessible(T member) { + member.setAccessible(true); + return member; + } + } + + private static class JUnitPlatformLauncherExecutor { + @SuppressWarnings("unused") // accessed via Reflection + private List run(Class testClass, String ruleName) { + Launcher launcher = LauncherFactory.create(); + List violations = new ArrayList<>(); + launcher.registerTestExecutionListeners(new TestExecutionListener() { + @Override + public void executionFinished(TestIdentifier identifier, TestExecutionResult result) { + result.getThrowable().ifPresent(t -> violations.add(t.getMessage())); + } + }); + // reference test class by class name to make sure FieldSelector can create the class + launcher.execute(request().selectors(selectField(testClass.getName(), ruleName)).build()); + return violations; + } + } +} diff --git a/archunit-integration-test/junit6/src/test/java/com/tngtech/archunit/integration/ExamplesJUnit6IntegrationTest.java b/archunit-integration-test/junit6/src/test/java/com/tngtech/archunit/integration/ExamplesJUnit6IntegrationTest.java new file mode 100644 index 0000000000..71917fd2f9 --- /dev/null +++ b/archunit-integration-test/junit6/src/test/java/com/tngtech/archunit/integration/ExamplesJUnit6IntegrationTest.java @@ -0,0 +1,140 @@ +package com.tngtech.archunit.integration; + +import java.util.stream.Stream; + +import com.tngtech.archunit.testutils.ExpectedJUnit6TestFailures; +import com.tngtech.archunit.testutils.ExpectedTestFailures; +import com.tngtech.archunit.testutils.ResultStoringExtension; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.TestFactory; + +class ExamplesJUnit6IntegrationTest extends ExamplesIntegrationTestBase { + + @BeforeAll + static void initExtension() { + ResultStoringExtension.enable(); + } + + @AfterEach + void tearDown() { + ResultStoringExtension.reset(); + } + + @AfterAll + static void disableExtension() { + ResultStoringExtension.disable(); + } + + @Override + protected ExpectedTestFailures createExpectedTestFailures(Class... testClasses) { + return ExpectedJUnit6TestFailures.forTests(testClasses); + } + + @TestFactory + Stream CodingRulesTest() { + return CodingRulesTest(com.tngtech.archunit.exampletest.junit6.CodingRulesTest.class); + } + + @TestFactory + Stream ControllerRulesTest() { + return ControllerRulesTest(com.tngtech.archunit.exampletest.junit6.ControllerRulesTest.class); + } + + @TestFactory + Stream CyclicDependencyRulesTest() { + return CyclicDependencyRulesTest(com.tngtech.archunit.exampletest.junit6.CyclicDependencyRulesTest.class); + } + + @TestFactory + Stream DaoRulesTest() { + return DaoRulesTest(com.tngtech.archunit.exampletest.junit6.DaoRulesTest.class); + } + + @TestFactory + Stream DependencyRulesTest() { + return DependencyRulesTest(com.tngtech.archunit.exampletest.junit6.DependencyRulesTest.class); + } + + @TestFactory + Stream FrozenRulesTest() { + return FrozenRulesTest(com.tngtech.archunit.exampletest.junit6.FrozenRulesTest.class); + } + + @TestFactory + Stream InterfaceRulesTest() { + return InterfaceRulesTest(com.tngtech.archunit.exampletest.junit6.InterfaceRulesTest.class); + } + + @TestFactory + Stream LayerDependencyRulesTest() { + return LayerDependencyRulesTest(com.tngtech.archunit.exampletest.junit6.LayerDependencyRulesTest.class); + } + + @TestFactory + Stream LayeredArchitectureTest() { + return LayeredArchitectureTest(com.tngtech.archunit.exampletest.junit6.LayeredArchitectureTest.class); + } + + @TestFactory + Stream OnionArchitectureTest() { + return OnionArchitectureTest(com.tngtech.archunit.exampletest.junit6.OnionArchitectureTest.class); + } + + @TestFactory + Stream MethodsTest() { + return MethodsTest(com.tngtech.archunit.exampletest.junit6.MethodsTest.class); + } + + @TestFactory + Stream ModulesTest() { + return ModulesTest(com.tngtech.archunit.exampletest.junit6.ModulesTest.class); + } + + @TestFactory + Stream NamingConventionTest() { + return NamingConventionTest(com.tngtech.archunit.exampletest.junit6.NamingConventionTest.class); + } + + @TestFactory + Stream PlantUmlArchitectureTest() { + return PlantUmlArchitectureTest(com.tngtech.archunit.exampletest.junit6.PlantUmlArchitectureTest.class); + } + + @TestFactory + Stream ProxyRulesTest() { + return ProxyRulesTest(com.tngtech.archunit.exampletest.junit6.ProxyRulesTest.class); + } + + @TestFactory + Stream RestrictNumberOfClassesWithACertainPropertyTest() { + return RestrictNumberOfClassesWithACertainPropertyTest(com.tngtech.archunit.exampletest.junit6.RestrictNumberOfClassesWithACertainPropertyTest.class); + } + + @TestFactory + Stream SecurityTest() { + return SecurityTestImpl(); + } + + @TestFactory + Stream SessionBeanRulesTest() { + return SessionBeanRulesTest(com.tngtech.archunit.exampletest.junit6.SessionBeanRulesTest.class); + } + + @TestFactory + Stream SingleClassTest() { + return SingleClassTest(com.tngtech.archunit.exampletest.junit6.SingleClassTest.class); + } + + @TestFactory + Stream SlicesIsolationTest() { + return SlicesIsolationTest(com.tngtech.archunit.exampletest.junit6.SlicesIsolationTest.class); + } + + @TestFactory + Stream ThirdPartyRulesTest() { + return ThirdPartyRulesTest(com.tngtech.archunit.exampletest.junit6.ThirdPartyRulesTest.class); + } +} diff --git a/archunit-integration-test/junit6/src/test/java/com/tngtech/archunit/integration/ExtensionIntegrationTest.java b/archunit-integration-test/junit6/src/test/java/com/tngtech/archunit/integration/ExtensionIntegrationTest.java new file mode 100644 index 0000000000..61b63341d1 --- /dev/null +++ b/archunit-integration-test/junit6/src/test/java/com/tngtech/archunit/integration/ExtensionIntegrationTest.java @@ -0,0 +1,84 @@ +package com.tngtech.archunit.integration; + +import java.util.Map; +import java.util.Objects; + +import com.tngtech.archunit.ArchConfiguration; +import com.tngtech.archunit.core.domain.JavaClasses; +import com.tngtech.archunit.core.importer.ClassFileImporter; +import com.tngtech.archunit.example.layers.ClassViolatingCodingRules; +import com.tngtech.archunit.exampletest.extension.EvaluatedRuleEvent; +import com.tngtech.archunit.exampletest.extension.ExampleExtension; +import com.tngtech.archunit.lang.ArchRule; +import org.assertj.core.api.Condition; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static com.google.common.collect.Iterables.getOnlyElement; +import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses; +import static org.assertj.core.api.Assertions.assertThat; + +class ExtensionIntegrationTest { + private final JavaClasses classes = new ClassFileImporter().importPackagesOf(ClassViolatingCodingRules.class); + + @BeforeEach + void setUp() { + ExampleExtension.reset(); + } + + @AfterEach + void tearDown() { + ExampleExtension.reset(); + ArchConfiguration.get().configureExtension(ExampleExtension.UNIQUE_IDENTIFIER) + .setProperty("enabled", false); + } + + @Test + void evaluation_results_are_dispatched_to_extensions() { + ArchConfiguration.get().configureExtension(ExampleExtension.UNIQUE_IDENTIFIER) + .setProperty("enabled", true); + + ArchRule rule = noClasses().should().haveFullyQualifiedName(ClassViolatingCodingRules.class.getName()); + checkRuleAndIgnoreFailure(classes, rule); + + assertThat(ExampleExtension.getConfigurationEvents()).hasSize(1); + assertThat(ExampleExtension.getConfigurationEvents()) + .extracting("properties").are(containingEntry("example-prop", "exampleValue")); + + EvaluatedRuleEvent event = getOnlyElement(ExampleExtension.getEvaluatedRuleEvents()); + assertThat(event.contains(rule)).as("Rule was passed").isTrue(); + assertThat(event.contains(classes)).as("Classes were passed").isTrue(); + assertThat(event.hasViolationFor(ClassViolatingCodingRules.class)) + .as("Has violation for " + ClassViolatingCodingRules.class.getSimpleName()).isTrue(); + } + + @Test + void evaluation_results_are_only_dispatched_to_enabled_extensions() { + ArchConfiguration.get().configureExtension(ExampleExtension.UNIQUE_IDENTIFIER) + .setProperty("enabled", false); + + ArchRule rule = noClasses().should().haveFullyQualifiedName(ClassViolatingCodingRules.class.getName()); + checkRuleAndIgnoreFailure(classes, rule); + + assertThat(ExampleExtension.getConfigurationEvents()).isEmpty(); + assertThat(ExampleExtension.getEvaluatedRuleEvents()).isEmpty(); + } + + private static Condition containingEntry(String propKey, String propValue) { + return new Condition(String.format("containing entry {%s=%s}", propKey, propValue)) { + @Override + public boolean matches(Object value) { + return Objects.equals(((Map) value).get(propKey), propValue); + } + }; + } + + private static void checkRuleAndIgnoreFailure(JavaClasses classes, ArchRule rule) { + try { + rule.check(classes); + throw new RuntimeException("Should have thrown an " + AssertionError.class.getSimpleName()); + } catch (AssertionError ignored) { + } + } +} diff --git a/archunit-integration-test/junit6/src/test/java/com/tngtech/archunit/testutils/ContextClassLoaderExtension.java b/archunit-integration-test/junit6/src/test/java/com/tngtech/archunit/testutils/ContextClassLoaderExtension.java new file mode 100644 index 0000000000..978de93684 --- /dev/null +++ b/archunit-integration-test/junit6/src/test/java/com/tngtech/archunit/testutils/ContextClassLoaderExtension.java @@ -0,0 +1,19 @@ +package com.tngtech.archunit.testutils; + +import org.junit.jupiter.api.extension.AfterEachCallback; +import org.junit.jupiter.api.extension.BeforeEachCallback; +import org.junit.jupiter.api.extension.ExtensionContext; + +public class ContextClassLoaderExtension implements BeforeEachCallback, AfterEachCallback { + private ClassLoader contextClassLoader; + + @Override + public void beforeEach(ExtensionContext context) { + contextClassLoader = Thread.currentThread().getContextClassLoader(); + } + + @Override + public void afterEach(ExtensionContext context) { + Thread.currentThread().setContextClassLoader(contextClassLoader); + } +} diff --git a/archunit-integration-test/junit6/src/test/java/com/tngtech/archunit/testutils/ExpectedJUnit6TestFailures.java b/archunit-integration-test/junit6/src/test/java/com/tngtech/archunit/testutils/ExpectedJUnit6TestFailures.java new file mode 100644 index 0000000000..a48766351f --- /dev/null +++ b/archunit-integration-test/junit6/src/test/java/com/tngtech/archunit/testutils/ExpectedJUnit6TestFailures.java @@ -0,0 +1,60 @@ +package com.tngtech.archunit.testutils; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; +import java.util.stream.Stream; + +import org.junit.jupiter.api.DynamicTest; +import org.junit.platform.engine.TestExecutionResult; +import org.junit.platform.launcher.Launcher; +import org.junit.platform.launcher.LauncherDiscoveryRequest; +import org.junit.platform.launcher.TestExecutionListener; +import org.junit.platform.launcher.TestIdentifier; +import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder; +import org.junit.platform.launcher.core.LauncherFactory; + +import static org.junit.jupiter.api.DynamicTest.dynamicTest; +import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass; + +public class ExpectedJUnit6TestFailures extends ExpectedTestFailures { + + ExpectedJUnit6TestFailures(Class[] testClasses) { + super(testClasses); + } + + @Override + public Stream toDynamicTests(Consumer aroundTestInvoke) { + return testClasses.stream() + .map(testClass -> new RunnableTest(ExpectedJUnit6TestFailures::runTests, testClass)) + .map(test -> dynamicTest( + test.getDisplayName(), + () -> aroundTestInvoke.accept(() -> assertActualAndExpectedViolationsMatch(test)))); + } + + public static ExpectedJUnit6TestFailures forTests(Class... testClasses) { + return new ExpectedJUnit6TestFailures(testClasses); + } + + private static List runTests(Class testClass) { + List result = new ArrayList<>(); + + LauncherDiscoveryRequest request = LauncherDiscoveryRequestBuilder.request() + .selectors(selectClass(testClass)) + .build(); + Launcher launcher = LauncherFactory.create(); + launcher.registerTestExecutionListeners(new TestExecutionListener() { + @Override + public void executionFinished(TestIdentifier testIdentifier, TestExecutionResult testExecutionResult) { + if (!testIdentifier.isContainer() && testExecutionResult.getStatus() == TestExecutionResult.Status.FAILED) { + testExecutionResult.getThrowable().ifPresent(throwable -> { + result.add(new TestFailure(testIdentifier.getDisplayName(), throwable)); + }); + } + } + }); + launcher.execute(request); + + return result; + } +} diff --git a/archunit-integration-test/junit6/src/test/resources/META-INF/services/com.tngtech.archunit.lang.extension.ArchUnitExtension b/archunit-integration-test/junit6/src/test/resources/META-INF/services/com.tngtech.archunit.lang.extension.ArchUnitExtension new file mode 100644 index 0000000000..fc462642ea --- /dev/null +++ b/archunit-integration-test/junit6/src/test/resources/META-INF/services/com.tngtech.archunit.lang.extension.ArchUnitExtension @@ -0,0 +1 @@ +com.tngtech.archunit.testutils.ResultStoringExtension \ No newline at end of file diff --git a/archunit-integration-test/junit6/src/test/resources/archunit.properties b/archunit-integration-test/junit6/src/test/resources/archunit.properties new file mode 100644 index 0000000000..fd12d83332 --- /dev/null +++ b/archunit-integration-test/junit6/src/test/resources/archunit.properties @@ -0,0 +1,5 @@ +import.dependencyResolutionProcess.maxIterationsForEnclosingTypes=0 +import.dependencyResolutionProcess.maxIterationsForGenericSignatureTypes=0 +extension.archunit-example-extension.enabled=false +extension.archunit-example-extension.example-prop=exampleValue +freeze.store.default.path=../../archunit-example/example-plain/src/test/resources/frozen diff --git a/archunit-integration-test/shared/src/main/java/com/tngtech/archunit/integration/ExamplesIntegrationTestBase.java b/archunit-integration-test/shared/src/main/java/com/tngtech/archunit/integration/ExamplesIntegrationTestBase.java index 2f1507a6df..db0b66d36b 100644 --- a/archunit-integration-test/shared/src/main/java/com/tngtech/archunit/integration/ExamplesIntegrationTestBase.java +++ b/archunit-integration-test/shared/src/main/java/com/tngtech/archunit/integration/ExamplesIntegrationTestBase.java @@ -616,7 +616,15 @@ Stream DependencyRulesTest(Class dependencyRulesTestClass) { abstract Stream FrozenRulesTest(); - Stream FrozenRulesTest(Class frozenRulesTestClass, Consumer withTemporaryViolationStore) { + Stream FrozenRulesTest(Class frozenRulesTestClass) { + return frozenRulesExpectedTestFailures(frozenRulesTestClass).toDynamicTests(); + } + + Stream FrozenRulesTest(Class frozenRulesTestClass, Consumer aroundTestInvoke) { + return frozenRulesExpectedTestFailures(frozenRulesTestClass).toDynamicTests(aroundTestInvoke); + } + + private ExpectedTestFailures frozenRulesExpectedTestFailures(Class frozenRulesTestClass) { return createExpectedTestFailures(com.tngtech.archunit.exampletest.FrozenRulesTest.class, frozenRulesTestClass) .ofRule("no classes should depend on classes that reside in a package '..service..'") @@ -639,9 +647,7 @@ Stream FrozenRulesTest(Class frozenRulesTestClass, Consumer with .inLine(26)) .by(callFromMethod(ServiceViolatingDaoRules.class, "illegallyUseEntityManager"). toMethod(ServiceViolatingDaoRules.MyEntityManager.class, "persist", Object.class) - .inLine(27)) - - .toDynamicTests(withTemporaryViolationStore); + .inLine(27)); } abstract Stream InterfaceRulesTest(); diff --git a/archunit-junit/build.gradle b/archunit-junit/build.gradle index 97b776e414..abdc144b54 100644 --- a/archunit-junit/build.gradle +++ b/archunit-junit/build.gradle @@ -59,27 +59,8 @@ shadowJar { } } -def addCleanThirdPartyTask = { - // These files are already relocated into archunit.jar, so they're transitively available - tasks.create(name: 'removeDuplicateThirdParty', type: Jar, dependsOn: shadowJar) { - exclude "${thirdPartyRelocationPackage.replace('.', '/')}/**" - - File tempPath = tempJar(jar.archiveFile.get().getAsFile()) - File jarPath = shadowJar.archiveFile.get().getAsFile() - inputs.file(jarPath) - from zipTree(jarPath) - archiveFileName = tempPath.name - - doLast { - assert jarPath.delete() - assert tempPath.renameTo(jarPath) - } - } - finishArchive.dependsOn removeDuplicateThirdParty -} ext.configureJUnitArchive = { configureDependencies -> return { - delegate.with addCleanThirdPartyTask compileJava.dependsOn project(':archunit-junit').finishArchive publishing{ @@ -94,6 +75,4 @@ ext.configureJUnitArchive = { configureDependencies -> } } -this.with addCleanThirdPartyTask - javadoc.enabled = false diff --git a/archunit-junit/junit4/build.gradle b/archunit-junit/junit4/build.gradle index 636adbbb3e..e385a27501 100644 --- a/archunit-junit/junit4/build.gradle +++ b/archunit-junit/junit4/build.gradle @@ -1,6 +1,7 @@ plugins { id 'archunit.java-release-conventions' id 'archunit.java-junit-conventions' + id 'archunit.self-test-conventions' } ext.moduleName = 'com.tngtech.archunit.junit4' @@ -45,7 +46,7 @@ shadowJar { def configureDependencies = { deps -> deps.children().removeIf { dep -> - dep.scope.text() != 'compile' || !(dep.artifactId.text() in ['archunit', 'archunit-junit5-api', 'archunit-junit5-engine-api', 'junit']) + dep.scope.text() != 'compile' || !(dep.artifactId.text() in ['archunit', 'junit']) } } this.with project(':archunit-junit').configureJUnitArchive(configureDependencies) diff --git a/archunit-junit/junit4/src/archTest/java/com/tngtech/archunit/junit/ArchUnitArchitectureTest.java b/archunit-junit/junit4/src/archTest/java/com/tngtech/archunit/junit/ArchUnitArchitectureTest.java new file mode 100644 index 0000000000..600c83d275 --- /dev/null +++ b/archunit-junit/junit4/src/archTest/java/com/tngtech/archunit/junit/ArchUnitArchitectureTest.java @@ -0,0 +1,16 @@ +package com.tngtech.archunit.junit; + +import com.tngtech.archunit.ArchitectureTestImportOptions.DoNotIncludeSelfTests; +import com.tngtech.archunit.PublicAPIRules; +import com.tngtech.archunit.core.importer.ImportOption.DoNotIncludeTests; +import org.junit.runner.RunWith; + +@RunWith(ArchUnitRunner.class) +@AnalyzeClasses(packages = "com.tngtech.archunit.junit", importOptions = {DoNotIncludeTests.class, DoNotIncludeSelfTests.class}) +public class ArchUnitArchitectureTest { + private ArchUnitArchitectureTest() { + } + + @ArchTest + public static final ArchTests public_api_rules = ArchTests.in(PublicAPIRules.class); +} diff --git a/archunit-junit/junit5/api/build.gradle b/archunit-junit/junit5/api/build.gradle index aaaed6c241..6506ad9b37 100644 --- a/archunit-junit/junit5/api/build.gradle +++ b/archunit-junit/junit5/api/build.gradle @@ -1,5 +1,6 @@ plugins { id 'archunit.java-release-conventions' + id 'archunit.self-test-conventions' } ext.moduleName = 'com.tngtech.archunit.junit5.api' diff --git a/archunit-junit/junit5/api/src/archTest/java/com/tngtech/archunit/junit/ArchUnitArchitectureTest.java b/archunit-junit/junit5/api/src/archTest/java/com/tngtech/archunit/junit/ArchUnitArchitectureTest.java new file mode 100644 index 0000000000..403b782f81 --- /dev/null +++ b/archunit-junit/junit5/api/src/archTest/java/com/tngtech/archunit/junit/ArchUnitArchitectureTest.java @@ -0,0 +1,14 @@ +package com.tngtech.archunit.junit; + +import com.tngtech.archunit.ArchitectureTestImportOptions.DoNotIncludeSelfTests; +import com.tngtech.archunit.PublicAPIRules; +import com.tngtech.archunit.core.importer.ImportOption.DoNotIncludeTests; + +@AnalyzeClasses(packages = "com.tngtech.archunit.junit", importOptions = {DoNotIncludeTests.class, DoNotIncludeSelfTests.class}) +public class ArchUnitArchitectureTest { + private ArchUnitArchitectureTest() { + } + + @ArchTest + public static final ArchTests public_api_rules = ArchTests.in(PublicAPIRules.class); +} diff --git a/archunit-junit/junit5/engine-api/build.gradle b/archunit-junit/junit5/engine-api/build.gradle index 2affc518d0..4dfaf92ea4 100644 --- a/archunit-junit/junit5/engine-api/build.gradle +++ b/archunit-junit/junit5/engine-api/build.gradle @@ -1,6 +1,7 @@ plugins { id 'archunit.java-release-conventions' id 'archunit.java-junit-conventions' + id 'archunit.self-test-conventions' } ext.moduleName = 'com.tngtech.archunit.junit5.engineapi' diff --git a/archunit-junit/junit5/engine-api/src/archTest/java/com/tngtech/archunit/junit/engine_api/ArchUnitArchitectureTest.java b/archunit-junit/junit5/engine-api/src/archTest/java/com/tngtech/archunit/junit/engine_api/ArchUnitArchitectureTest.java new file mode 100644 index 0000000000..699e98809f --- /dev/null +++ b/archunit-junit/junit5/engine-api/src/archTest/java/com/tngtech/archunit/junit/engine_api/ArchUnitArchitectureTest.java @@ -0,0 +1,17 @@ +package com.tngtech.archunit.junit.engine_api; + +import com.tngtech.archunit.PublicAPIRules; +import com.tngtech.archunit.ArchitectureTestImportOptions.DoNotIncludeSelfTests; +import com.tngtech.archunit.core.importer.ImportOption.DoNotIncludeTests; +import com.tngtech.archunit.junit.AnalyzeClasses; +import com.tngtech.archunit.junit.ArchTest; +import com.tngtech.archunit.junit.ArchTests; + +@AnalyzeClasses(packages = "com.tngtech.archunit.junit.engine_api", importOptions = {DoNotIncludeTests.class, DoNotIncludeSelfTests.class}) +public class ArchUnitArchitectureTest { + private ArchUnitArchitectureTest() { + } + + @ArchTest + public static final ArchTests public_api_rules = ArchTests.in(PublicAPIRules.class); +} diff --git a/archunit-junit/junit5/engine/build.gradle b/archunit-junit/junit5/engine/build.gradle index 6c7371fd95..87a53ffba5 100644 --- a/archunit-junit/junit5/engine/build.gradle +++ b/archunit-junit/junit5/engine/build.gradle @@ -1,6 +1,7 @@ plugins { id 'archunit.java-release-conventions' id 'archunit.java-junit-conventions' + id 'archunit.self-test-conventions' } ext.moduleName = 'com.tngtech.archunit.junit5.engine' diff --git a/archunit-junit/junit5/engine/src/archTest/java/com/tngtech/archunit/junit/internal/ArchUnitArchitectureTest.java b/archunit-junit/junit5/engine/src/archTest/java/com/tngtech/archunit/junit/internal/ArchUnitArchitectureTest.java new file mode 100644 index 0000000000..f5cb18623c --- /dev/null +++ b/archunit-junit/junit5/engine/src/archTest/java/com/tngtech/archunit/junit/internal/ArchUnitArchitectureTest.java @@ -0,0 +1,17 @@ +package com.tngtech.archunit.junit.internal; + +import com.tngtech.archunit.ArchitectureTestImportOptions.DoNotIncludeSelfTests; +import com.tngtech.archunit.PublicAPIRules; +import com.tngtech.archunit.core.importer.ImportOption.DoNotIncludeTests; +import com.tngtech.archunit.junit.AnalyzeClasses; +import com.tngtech.archunit.junit.ArchTest; +import com.tngtech.archunit.junit.ArchTests; + +@AnalyzeClasses(packages = "com.tngtech.archunit.junit.internal", importOptions = {DoNotIncludeTests.class, DoNotIncludeSelfTests.class}) +public class ArchUnitArchitectureTest { + private ArchUnitArchitectureTest() { + } + + @ArchTest + public static final ArchTests public_api_rules = ArchTests.in(PublicAPIRules.class); +} diff --git a/archunit-junit/junit6/aggregator/build.gradle b/archunit-junit/junit6/aggregator/build.gradle new file mode 100644 index 0000000000..6f8c003466 --- /dev/null +++ b/archunit-junit/junit6/aggregator/build.gradle @@ -0,0 +1,16 @@ +plugins { + id 'archunit.java-release-conventions' +} + +ext.moduleName = 'com.tngtech.archunit.junit6' + +ext.minimumJavaVersion = JavaVersion.VERSION_17 + +dependencies { + api(project(":archunit-junit6-api")) + implementation(project(":archunit-junit6-engine")) +} + +shadowJar { + exclude '**' +} diff --git a/archunit-junit/junit6/api/build.gradle b/archunit-junit/junit6/api/build.gradle new file mode 100644 index 0000000000..75abec9d1a --- /dev/null +++ b/archunit-junit/junit6/api/build.gradle @@ -0,0 +1,40 @@ +plugins { + id 'archunit.java-release-conventions' + id 'archunit.self-test-conventions' +} + +ext.moduleName = 'com.tngtech.archunit.junit6.api' + +ext.minimumJavaVersion = JavaVersion.VERSION_17 + +dependencies { + api project(path: ':archunit') + api project(path: ':archunit-junit', configuration: 'archJunitApi') + api libs.junit6PlatformCommons +} + +javadoc { + source(project(':archunit-junit').sourceSets.archJunitApi.allJava) +} +sourcesJar { + from project(':archunit-junit').sourceSets.archJunitApi.allSource +} + +shadowJar { + exclude 'META-INF/maven/**' + + dependencies { + exclude { !it.name.contains('archunit-junit') } + } +} + +def configureDependencies = { deps -> + deps.children().removeIf { dep -> + dep.scope.text() != 'compile' || !(dep.artifactId.text() in ['archunit']) + } +} +this.with project(':archunit-junit').configureJUnitArchive(configureDependencies) + +singlePackageExport { + exportedPackage = 'com.tngtech.archunit.junit' +} diff --git a/archunit-junit/junit6/api/src/archTest/java/com/tngtech/archunit/junit/ArchUnitArchitectureTest.java b/archunit-junit/junit6/api/src/archTest/java/com/tngtech/archunit/junit/ArchUnitArchitectureTest.java new file mode 100644 index 0000000000..2dda00c600 --- /dev/null +++ b/archunit-junit/junit6/api/src/archTest/java/com/tngtech/archunit/junit/ArchUnitArchitectureTest.java @@ -0,0 +1,14 @@ +package com.tngtech.archunit.junit; + +import com.tngtech.archunit.ArchitectureTestImportOptions.DoNotIncludeSelfTests; +import com.tngtech.archunit.PublicAPIRules; +import com.tngtech.archunit.core.importer.ImportOption.DoNotIncludeTests; + +@AnalyzeClasses(packages = "com.tngtech.archunit.junit", importOptions = {DoNotIncludeTests.class, DoNotIncludeSelfTests.class}) +public class ArchUnitArchitectureTest { + private ArchUnitArchitectureTest() { + } + + @ArchTest + public static final ArchTests public_api_rules = ArchTests.in(PublicAPIRules.class); +} diff --git a/archunit-junit/junit6/api/src/main/java/com/tngtech/archunit/junit/AnalyzeClasses.java b/archunit-junit/junit6/api/src/main/java/com/tngtech/archunit/junit/AnalyzeClasses.java new file mode 100644 index 0000000000..e26f61f50b --- /dev/null +++ b/archunit-junit/junit6/api/src/main/java/com/tngtech/archunit/junit/AnalyzeClasses.java @@ -0,0 +1,90 @@ +/* + * Copyright 2014-2026 TNG Technology Consulting GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.tngtech.archunit.junit; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import com.tngtech.archunit.PublicAPI; +import com.tngtech.archunit.core.domain.JavaClasses; +import com.tngtech.archunit.core.importer.ClassFileImporter; +import com.tngtech.archunit.core.importer.ImportOption; +import com.tngtech.archunit.core.importer.ImportOption.DoNotIncludeJars; +import com.tngtech.archunit.core.importer.ImportOption.DoNotIncludeTests; +import org.junit.platform.commons.annotation.Testable; + +import static com.tngtech.archunit.PublicAPI.Usage.ACCESS; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * Specifies which packages/locations should be scanned and tested when running a JUnit 5 test. + *

+ * To ignore certain classes (e.g. classes in test scope) see {@link #importOptions()}, in particular {@link DoNotIncludeTests} and + * {@link DoNotIncludeJars}. + *

+ * When checking rules, it is important to remember that all relevant information/classes need to be imported for the rules + * to work. For example, if class A accesses class B and class B extends class C, but class B is not imported, then + * 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}. + * + * @see ClassFileImporter + */ +@Testable +@Target(TYPE) +@Retention(RUNTIME) +@PublicAPI(usage = ACCESS) +public @interface AnalyzeClasses { + /** + * @return Packages to look for within the classpath / modulepath + */ + String[] packages() default {}; + + /** + * @return Classes that specify packages to look for within the classpath / modulepath + */ + Class[] packagesOf() default {}; + + /** + * @return Implementations of {@link LocationProvider}. Allows to completely customize the sources, + * where classes are imported from. + */ + Class[] locations() default {}; + + /** + * @return Whether to look for classes on the whole classpath. + * Warning: Without any further {@link #importOptions() filtering} this can require a lot of resources. + */ + boolean wholeClasspath() default false; + + /** + * Allows to filter the class import. The supplied types will be instantiated and used to create the + * {@link ImportOption ImportOptions} passed to the {@link ClassFileImporter}. Considering caching, compare the notes on + * {@link ImportOption}. + * + * @return The types of {@link ImportOption} to use for the import + */ + Class[] importOptions() default {}; + + /** + * Controls, if {@link JavaClasses} should be cached by location, + * to be reused between several test classes, or just within the same class. + * + * @return The {@link CacheMode} to use for this test class. + */ + CacheMode cacheMode() default CacheMode.FOREVER; +} diff --git a/archunit-junit/junit6/api/src/main/java/com/tngtech/archunit/junit/ArchTag.java b/archunit-junit/junit6/api/src/main/java/com/tngtech/archunit/junit/ArchTag.java new file mode 100644 index 0000000000..124b5b76e4 --- /dev/null +++ b/archunit-junit/junit6/api/src/main/java/com/tngtech/archunit/junit/ArchTag.java @@ -0,0 +1,74 @@ +/* + * Copyright 2014-2026 TNG Technology Consulting GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.tngtech.archunit.junit; + +import java.lang.annotation.Documented; +import java.lang.annotation.Inherited; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import com.tngtech.archunit.PublicAPI; + +import static com.tngtech.archunit.PublicAPI.Usage.ACCESS; +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * {@link ArchTag @ArchTag} is a {@linkplain Repeatable repeatable} annotation that allows + * tagging any {@link ArchTest @ArchTest} field/method/class. Sets of rules can be classified to run together this way. + * + * Rules could be tagged like + *

+ *

+ *{@literal @}ArchTag("dependencies")
+ * static ArchRule no_accesses_from_server_to_client = classes()...
+ * 
+ * + * Users of {@link ArchTag} must follow the syntax conventions that the JUnit Platform Engine defines: + * + *
    + *
  • A tag must not be blank.
  • + *
  • A trimmed tag must not contain whitespace.
  • + *
  • A trimmed tag must not contain ISO control characters.
  • + *
  • A trimmed tag must not contain any of the following + * reserved characters. + *
      + *
    • {@code ,}: comma
    • + *
    • {@code (}: left parenthesis
    • + *
    • {@code )}: right parenthesis
    • + *
    • {@code &}: ampersand
    • + *
    • {@code |}: vertical bar
    • + *
    • {@code !}: exclamation point
    • + *
    + *
  • + *
+ */ +@Inherited +@Documented +@Retention(RUNTIME) +@PublicAPI(usage = ACCESS) +@Repeatable(ArchTags.class) +@Target({TYPE, METHOD, FIELD}) +public @interface ArchTag { + /** + * The actual tag. It will first be {@linkplain String#trim() trimmed} and must then adhere to the + * {@linkplain ArchTag Syntax Rules for Tags}. Otherwise the tag will be ignored. + */ + String value(); +} diff --git a/archunit-junit/junit6/api/src/main/java/com/tngtech/archunit/junit/ArchTags.java b/archunit-junit/junit6/api/src/main/java/com/tngtech/archunit/junit/ArchTags.java new file mode 100644 index 0000000000..2346e72e7e --- /dev/null +++ b/archunit-junit/junit6/api/src/main/java/com/tngtech/archunit/junit/ArchTags.java @@ -0,0 +1,48 @@ +/* + * Copyright 2014-2026 TNG Technology Consulting GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.tngtech.archunit.junit; + +import java.lang.annotation.Documented; +import java.lang.annotation.Inherited; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import com.tngtech.archunit.Internal; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * Simply a container for {@link ArchTag}. Should never be used directly, but instead + * {@link ArchTag} should be used in a {@linkplain Repeatable repeatable} manner, e.g. + *

+ *

+ *{@literal @}ArchTag("foo")
+ *{@literal @}ArchTag("bar")
+ * static ArchRule example = classes()...
+ * 
+ */ +@Internal +@Inherited +@Documented +@Retention(RUNTIME) +@Target({TYPE, METHOD, FIELD}) +public @interface ArchTags { + ArchTag[] value(); +} diff --git a/archunit-junit/junit6/api/src/main/java/com/tngtech/archunit/junit/ArchTest.java b/archunit-junit/junit6/api/src/main/java/com/tngtech/archunit/junit/ArchTest.java new file mode 100644 index 0000000000..f72103ef5b --- /dev/null +++ b/archunit-junit/junit6/api/src/main/java/com/tngtech/archunit/junit/ArchTest.java @@ -0,0 +1,54 @@ +/* + * Copyright 2014-2026 TNG Technology Consulting GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.tngtech.archunit.junit; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import com.tngtech.archunit.core.domain.JavaClasses; +import com.tngtech.archunit.lang.ArchRule; +import org.junit.platform.commons.annotation.Testable; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * Marks ArchUnit tests to be executed by the test infrastructure. These tests can have the following form: + *
    + *
  • + * A static field of type {@link ArchRule} -> this rule will automatically be checked against the imported classes + *
  • + *
  • + * A static method with one parameter {@link JavaClasses} -> this method will be called with the imported classes + *
  • + *
+ *
Example: + *

+ *{@literal @}ArchTest
+ * public static final ArchRule someRule = classes()... ;
+ *
+ *{@literal @}ArchTest
+ * public static void someMethod(JavaClasses classes) {
+ *     // do something with classes
+ * }
+ * 
+ */ +@Testable +@Target({FIELD, METHOD}) +@Retention(RUNTIME) +public @interface ArchTest { +} diff --git a/archunit-junit/junit6/engine-api/build.gradle b/archunit-junit/junit6/engine-api/build.gradle new file mode 100644 index 0000000000..3c8aeddcb1 --- /dev/null +++ b/archunit-junit/junit6/engine-api/build.gradle @@ -0,0 +1,39 @@ +plugins { + id 'archunit.java-release-conventions' + id 'archunit.java-junit-conventions' + id 'archunit.self-test-conventions' +} + +ext.moduleName = 'com.tngtech.archunit.junit6.engineapi' + +ext.minimumJavaVersion = JavaVersion.VERSION_17 + +dependencies { + api libs.junit6PlatformEngine + implementation project(path: ':archunit') + + testImplementation project(path: ':archunit-junit6-api') + testImplementation libs.assertj +} + +compileJava.dependsOn project(':archunit-junit6-api').finishArchive + +shadowJar { + exclude 'META-INF/maven/**' + + dependencies { + exclude { true } + } +} + +// dependencies to archunit only cover annotations; we can skip those without breaking consumers to keep the dependency slim +def configureDependencies = { deps -> + deps.children().removeIf{ dep -> + dep.artifactId.text() != 'junit-platform-engine' + } +} +this.with project(':archunit-junit').configureJUnitArchive(configureDependencies) + +singlePackageExport { + exportedPackage = 'com.tngtech.archunit.junit.engine_api' +} diff --git a/archunit-junit/junit6/engine-api/src/archTest/java/com/tngtech/archunit/junit/engine_api/ArchUnitArchitectureTest.java b/archunit-junit/junit6/engine-api/src/archTest/java/com/tngtech/archunit/junit/engine_api/ArchUnitArchitectureTest.java new file mode 100644 index 0000000000..699e98809f --- /dev/null +++ b/archunit-junit/junit6/engine-api/src/archTest/java/com/tngtech/archunit/junit/engine_api/ArchUnitArchitectureTest.java @@ -0,0 +1,17 @@ +package com.tngtech.archunit.junit.engine_api; + +import com.tngtech.archunit.PublicAPIRules; +import com.tngtech.archunit.ArchitectureTestImportOptions.DoNotIncludeSelfTests; +import com.tngtech.archunit.core.importer.ImportOption.DoNotIncludeTests; +import com.tngtech.archunit.junit.AnalyzeClasses; +import com.tngtech.archunit.junit.ArchTest; +import com.tngtech.archunit.junit.ArchTests; + +@AnalyzeClasses(packages = "com.tngtech.archunit.junit.engine_api", importOptions = {DoNotIncludeTests.class, DoNotIncludeSelfTests.class}) +public class ArchUnitArchitectureTest { + private ArchUnitArchitectureTest() { + } + + @ArchTest + public static final ArchTests public_api_rules = ArchTests.in(PublicAPIRules.class); +} diff --git a/archunit-junit/junit6/engine-api/src/main/java/com/tngtech/archunit/junit/engine_api/FieldSelector.java b/archunit-junit/junit6/engine-api/src/main/java/com/tngtech/archunit/junit/engine_api/FieldSelector.java new file mode 100644 index 0000000000..6568894eed --- /dev/null +++ b/archunit-junit/junit6/engine-api/src/main/java/com/tngtech/archunit/junit/engine_api/FieldSelector.java @@ -0,0 +1,115 @@ +/* + * Copyright 2014-2026 TNG Technology Consulting GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.tngtech.archunit.junit.engine_api; + +import java.lang.reflect.Field; +import java.util.Objects; + +import com.tngtech.archunit.Internal; +import com.tngtech.archunit.PublicAPI; +import com.tngtech.archunit.base.ClassLoaders; +import com.tngtech.archunit.base.MayResolveTypesViaReflection; +import org.junit.platform.engine.DiscoverySelector; + +import static com.tngtech.archunit.PublicAPI.Usage.ACCESS; + +@PublicAPI(usage = ACCESS) +public final class FieldSelector implements DiscoverySelector { + private final Class clazz; + private final Field field; + + private FieldSelector(Class clazz, Field field) { + this.clazz = clazz; + this.field = field; + } + + @Internal + public Class getJavaClass() { + return clazz; + } + + @Internal + public Field getJavaField() { + return field; + } + + @Override + public int hashCode() { + return Objects.hash(clazz, field); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + FieldSelector other = (FieldSelector) obj; + return Objects.equals(this.clazz, other.clazz) + && Objects.equals(this.field, other.field); + } + + @Override + public String toString() { + return getClass().getSimpleName() + "{" + + "clazz=" + clazz + + ", field=" + field + + '}'; + } + + @PublicAPI(usage = ACCESS) + public static FieldSelector selectField(String className, String fieldName) { + return selectField(classForName(className), fieldName); + } + + @MayResolveTypesViaReflection(reason = "Within the ArchUnitTestEngine we may resolve types via reflection, since they are needed anyway") + private static Class classForName(String className) { + try { + return ClassLoaders.loadClass(className); + } catch (ClassNotFoundException e) { + throw new IllegalArgumentException("Could not load requested class " + className, e); + } + } + + @PublicAPI(usage = ACCESS) + public static FieldSelector selectField(Class javaClass, String fieldName) { + Field selectedField = findFieldInHierarchy(javaClass, fieldName); + if (selectedField == null) { + throw new IllegalArgumentException(String.format("Could not find field %s.%s", javaClass.getName(), fieldName)); + } + return selectField(javaClass, selectedField); + } + + private static Field findFieldInHierarchy(Class javaClass, String fieldName) { + Field selectedField = null; + Class currentClass = javaClass; + while (selectedField == null && !currentClass.equals(Object.class)) { + try { + selectedField = currentClass.getDeclaredField(fieldName); + } catch (NoSuchFieldException ignored) { + currentClass = currentClass.getSuperclass(); + } + } + return selectedField; + } + + @PublicAPI(usage = ACCESS) + public static FieldSelector selectField(Class javaClass, Field field) { + return new FieldSelector(javaClass, field); + } +} diff --git a/archunit-junit/junit6/engine-api/src/main/java/com/tngtech/archunit/junit/engine_api/FieldSource.java b/archunit-junit/junit6/engine-api/src/main/java/com/tngtech/archunit/junit/engine_api/FieldSource.java new file mode 100644 index 0000000000..4025bc39bf --- /dev/null +++ b/archunit-junit/junit6/engine-api/src/main/java/com/tngtech/archunit/junit/engine_api/FieldSource.java @@ -0,0 +1,79 @@ +/* + * Copyright 2014-2026 TNG Technology Consulting GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.tngtech.archunit.junit.engine_api; + +import java.lang.reflect.Field; +import java.util.Objects; + +import com.tngtech.archunit.Internal; +import com.tngtech.archunit.PublicAPI; +import org.junit.platform.engine.TestSource; + +import static com.tngtech.archunit.PublicAPI.Usage.ACCESS; + +@PublicAPI(usage = ACCESS) +public final class FieldSource implements TestSource { + private final Class javaClass; + private final String fieldName; + + private FieldSource(Field field) { + javaClass = field.getDeclaringClass(); + fieldName = field.getName(); + } + + @PublicAPI(usage = ACCESS) + public String getClassName() { + return javaClass.getName(); + } + + @PublicAPI(usage = ACCESS) + public Class getJavaClass() { + return javaClass; + } + + @PublicAPI(usage = ACCESS) + public String getFieldName() { + return fieldName; + } + + @Override + public int hashCode() { + return Objects.hash(javaClass, fieldName); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + FieldSource other = (FieldSource) obj; + return Objects.equals(this.javaClass, other.javaClass) + && Objects.equals(this.fieldName, other.fieldName); + } + + @Override + public String toString() { + return getClass().getSimpleName() + "{" + getClassName() + '.' + fieldName + '}'; + } + + @Internal + public static FieldSource from(Field field) { + return new FieldSource(field); + } +} diff --git a/archunit-junit/junit6/engine-api/src/test/java/com/tngtech/archunit/junit/engine_api/FieldSelectorTest.java b/archunit-junit/junit6/engine-api/src/test/java/com/tngtech/archunit/junit/engine_api/FieldSelectorTest.java new file mode 100644 index 0000000000..130b8256e5 --- /dev/null +++ b/archunit-junit/junit6/engine-api/src/test/java/com/tngtech/archunit/junit/engine_api/FieldSelectorTest.java @@ -0,0 +1,48 @@ +package com.tngtech.archunit.junit.engine_api; + +import com.tngtech.archunit.junit.ArchTest; +import com.tngtech.archunit.lang.ArchRule; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class FieldSelectorTest { + + @Test + void select_simple_field() { + FieldSelector selector = FieldSelector.selectField(SomeParentTest.class.getName(), "someRule"); + + assertThat(selector.getJavaClass()).isEqualTo(SomeParentTest.class); + assertThat(selector.getJavaField().getName()).isEqualTo("someRule"); + } + + @Test + void select_parent_field_in_child() { + FieldSelector selector = FieldSelector.selectField(SomeChildTest.class.getName(), "someRule"); + + assertThat(selector.getJavaClass()).isEqualTo(SomeChildTest.class); + assertThat(selector.getJavaField().getName()).isEqualTo("someRule"); + assertThat(selector.getJavaField().getDeclaringClass()).isEqualTo(SomeParentTest.class); + } + + @Test + void select_shadowed_field() { + FieldSelector selector = FieldSelector.selectField(SomeChildTest.class.getName(), "someShadowedRule"); + + assertThat(selector.getJavaClass()).isEqualTo(SomeChildTest.class); + assertThat(selector.getJavaField().getName()).isEqualTo("someShadowedRule"); + assertThat(selector.getJavaField().getDeclaringClass()).isEqualTo(SomeChildTest.class); + } + + static class SomeParentTest { + @ArchTest + static final ArchRule someRule = null; + @ArchTest + static final ArchRule someShadowedRule = null; + } + + static class SomeChildTest extends SomeParentTest { + @ArchTest + static final ArchRule someShadowedRule = null; + } +} diff --git a/archunit-junit/junit6/engine/build.gradle b/archunit-junit/junit6/engine/build.gradle new file mode 100644 index 0000000000..e6a9fe88a2 --- /dev/null +++ b/archunit-junit/junit6/engine/build.gradle @@ -0,0 +1,65 @@ +plugins { + id 'archunit.java-release-conventions' + id 'archunit.java-junit-conventions' + id 'archunit.self-test-conventions' +} + +ext.moduleName = 'com.tngtech.archunit.junit6.engine' + +ext.minimumJavaVersion = JavaVersion.VERSION_17 + +dependencies { + api project(path: ':archunit') + api project(path: ':archunit-junit6-api') + api project(path: ':archunit-junit6-engine-api') + implementation project(path: ':archunit-junit') + dependency.addGuava { dependencyNotation, config -> implementation(dependencyNotation, config) } + implementation libs.slf4j + + testImplementation project(path: ':archunit', configuration: 'tests') + testImplementation libs.assertj + testImplementation libs.mockito + // No support for JUnit 6 in Mockito as of 2026-06. See https://github.com/mockito/mockito/pull/3781 + testImplementation libs.mockito.junit5 + testImplementation libs.log4j.core +} + +gradle.projectsEvaluated { + compileJava.dependsOn project(':archunit-junit6-api').finishArchive + compileJava.dependsOn project(':archunit-junit6-engine-api').finishArchive +} + +javadoc { + source(project(':archunit-junit').sourceSets.main.allJava) +} +sourcesJar { + from project(':archunit-junit').sourceSets.main.allSource +} + +test { + useJUnitPlatform { + excludeEngines 'archunit' + } +} +shadowJar { + exclude 'META-INF/maven/**' + + dependencies { + exclude { + def isApi = it.configuration == 'archJunitApi' + def isUnwantedDependency = it.moduleName != 'archunit-junit' + isUnwantedDependency || isApi + } + } +} + +def configureDependencies = { deps -> + deps.children().removeIf { dep -> + dep.scope.text() != 'compile' || !(dep.artifactId.text() in ['archunit', 'archunit-junit6-api', 'archunit-junit6-engine-api']) + } +} +this.with project(':archunit-junit').configureJUnitArchive(configureDependencies) + +singlePackageExport { + exportedPackage = 'com.tngtech.archunit.junit.internal' +} diff --git a/archunit-junit/junit6/engine/src/archTest/java/com/tngtech/archunit/junit/internal/ArchUnitArchitectureTest.java b/archunit-junit/junit6/engine/src/archTest/java/com/tngtech/archunit/junit/internal/ArchUnitArchitectureTest.java new file mode 100644 index 0000000000..f5cb18623c --- /dev/null +++ b/archunit-junit/junit6/engine/src/archTest/java/com/tngtech/archunit/junit/internal/ArchUnitArchitectureTest.java @@ -0,0 +1,17 @@ +package com.tngtech.archunit.junit.internal; + +import com.tngtech.archunit.ArchitectureTestImportOptions.DoNotIncludeSelfTests; +import com.tngtech.archunit.PublicAPIRules; +import com.tngtech.archunit.core.importer.ImportOption.DoNotIncludeTests; +import com.tngtech.archunit.junit.AnalyzeClasses; +import com.tngtech.archunit.junit.ArchTest; +import com.tngtech.archunit.junit.ArchTests; + +@AnalyzeClasses(packages = "com.tngtech.archunit.junit.internal", importOptions = {DoNotIncludeTests.class, DoNotIncludeSelfTests.class}) +public class ArchUnitArchitectureTest { + private ArchUnitArchitectureTest() { + } + + @ArchTest + public static final ArchTests public_api_rules = ArchTests.in(PublicAPIRules.class); +} diff --git a/archunit-junit/junit6/engine/src/main/java/com/tngtech/archunit/junit/internal/AbstractArchUnitTestDescriptor.java b/archunit-junit/junit6/engine/src/main/java/com/tngtech/archunit/junit/internal/AbstractArchUnitTestDescriptor.java new file mode 100644 index 0000000000..74209be21d --- /dev/null +++ b/archunit-junit/junit6/engine/src/main/java/com/tngtech/archunit/junit/internal/AbstractArchUnitTestDescriptor.java @@ -0,0 +1,86 @@ +/* + * Copyright 2014-2026 TNG Technology Consulting GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.tngtech.archunit.junit.internal; + +import java.lang.reflect.AnnotatedElement; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Stream; + +import com.tngtech.archunit.core.domain.Formatters; +import com.tngtech.archunit.junit.ArchIgnore; +import com.tngtech.archunit.junit.ArchTag; +import org.junit.platform.commons.support.AnnotationSupport; +import org.junit.platform.engine.TestDescriptor; +import org.junit.platform.engine.TestSource; +import org.junit.platform.engine.TestTag; +import org.junit.platform.engine.UniqueId; +import org.junit.platform.engine.support.descriptor.AbstractTestDescriptor; +import org.junit.platform.engine.support.hierarchical.Node; + +import static java.util.Collections.emptySet; +import static java.util.stream.Collectors.joining; +import static java.util.stream.Collectors.toSet; + +abstract class AbstractArchUnitTestDescriptor extends AbstractTestDescriptor implements Node { + private final Set tags; + private final SkipResult skipResult; + + AbstractArchUnitTestDescriptor(UniqueId uniqueId, String displayName, TestSource source, AnnotatedElement... elements) { + super(uniqueId, displayName, source); + tags = Arrays.stream(elements).map(this::findTagsOn).flatMap(Collection::stream).collect(toSet()); + skipResult = Arrays.stream(elements) + .map(e -> AnnotationSupport.findAnnotation(e, ArchIgnore.class)) + .filter(Optional::isPresent) + .map(Optional::get) + .findFirst() + .map(ignore -> SkipResult.skip(ignore.reason())) + .orElse(SkipResult.doNotSkip()); + } + + private Set findTagsOn(AnnotatedElement annotatedElement) { + return AnnotationSupport.findRepeatableAnnotations(annotatedElement, ArchTag.class) + .stream() + .map(annotation -> TestTag.create(annotation.value())) + .collect(toSet()); + } + + @Override + public SkipResult shouldBeSkipped(ArchUnitEngineExecutionContext context) { + return skipResult; + } + + @Override + public Set getTags() { + Set result = new HashSet<>(tags); + result.addAll(getParent().map(TestDescriptor::getTags).orElse(emptySet())); + return result; + } + + static String formatWithPath(UniqueId uniqueId, String name) { + return Stream.concat( + uniqueId.getSegments().stream() + .filter(it -> it.getType().equals(ArchUnitTestDescriptor.CLASS_SEGMENT_TYPE)) + .skip(1) + .map(UniqueId.Segment::getValue) + .map(Formatters::ensureSimpleName), + Stream.of(name) + ).collect(joining(" > ")); + } +} diff --git a/archunit-junit/junit6/engine/src/main/java/com/tngtech/archunit/junit/internal/ArchUnitEngineDescriptor.java b/archunit-junit/junit6/engine/src/main/java/com/tngtech/archunit/junit/internal/ArchUnitEngineDescriptor.java new file mode 100644 index 0000000000..a8668217ff --- /dev/null +++ b/archunit-junit/junit6/engine/src/main/java/com/tngtech/archunit/junit/internal/ArchUnitEngineDescriptor.java @@ -0,0 +1,26 @@ +/* + * Copyright 2014-2026 TNG Technology Consulting GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.tngtech.archunit.junit.internal; + +import org.junit.platform.engine.UniqueId; +import org.junit.platform.engine.support.descriptor.EngineDescriptor; +import org.junit.platform.engine.support.hierarchical.Node; + +class ArchUnitEngineDescriptor extends EngineDescriptor implements Node { + ArchUnitEngineDescriptor(UniqueId uniqueId) { + super(uniqueId, "ArchUnit JUnit 6"); + } +} diff --git a/archunit-junit/junit6/engine/src/main/java/com/tngtech/archunit/junit/internal/ArchUnitEngineExecutionContext.java b/archunit-junit/junit6/engine/src/main/java/com/tngtech/archunit/junit/internal/ArchUnitEngineExecutionContext.java new file mode 100644 index 0000000000..3aa6161340 --- /dev/null +++ b/archunit-junit/junit6/engine/src/main/java/com/tngtech/archunit/junit/internal/ArchUnitEngineExecutionContext.java @@ -0,0 +1,21 @@ +/* + * Copyright 2014-2026 TNG Technology Consulting GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.tngtech.archunit.junit.internal; + +import org.junit.platform.engine.support.hierarchical.EngineExecutionContext; + +class ArchUnitEngineExecutionContext implements EngineExecutionContext { +} diff --git a/archunit-junit/junit6/engine/src/main/java/com/tngtech/archunit/junit/internal/ArchUnitSystemPropertyTestFilterJUnit6.java b/archunit-junit/junit6/engine/src/main/java/com/tngtech/archunit/junit/internal/ArchUnitSystemPropertyTestFilterJUnit6.java new file mode 100644 index 0000000000..1f4bd56d8a --- /dev/null +++ b/archunit-junit/junit6/engine/src/main/java/com/tngtech/archunit/junit/internal/ArchUnitSystemPropertyTestFilterJUnit6.java @@ -0,0 +1,61 @@ +/* + * Copyright 2014-2026 TNG Technology Consulting GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.tngtech.archunit.junit.internal; + +import java.util.List; +import java.util.Set; +import java.util.function.Predicate; + +import com.google.common.base.Splitter; +import com.google.common.collect.ImmutableSet; +import com.tngtech.archunit.ArchConfiguration; +import org.junit.platform.engine.TestDescriptor; +import org.junit.platform.engine.UniqueId; + +import static com.tngtech.archunit.junit.internal.ArchUnitTestDescriptor.FIELD_SEGMENT_TYPE; +import static com.tngtech.archunit.junit.internal.ArchUnitTestDescriptor.METHOD_SEGMENT_TYPE; + +class ArchUnitSystemPropertyTestFilterJUnit6 { + private static final String JUNIT_TEST_FILTER_PROPERTY_NAME = "junit.testFilter"; + private static final Set MEMBER_SEGMENT_TYPES = ImmutableSet.of(FIELD_SEGMENT_TYPE, METHOD_SEGMENT_TYPE); + + void filter(TestDescriptor descriptor) { + ArchConfiguration configuration = ArchConfiguration.get(); + if (!configuration.containsProperty(JUNIT_TEST_FILTER_PROPERTY_NAME)) { + return; + } + + String testFilterProperty = configuration.getProperty(JUNIT_TEST_FILTER_PROPERTY_NAME); + List memberNames = Splitter.on(",").splitToList(testFilterProperty); + Predicate shouldRunPredicate = testDescriptor -> memberNameMatches(testDescriptor, memberNames); + removeNonMatching(descriptor, shouldRunPredicate); + } + + private void removeNonMatching(TestDescriptor descriptor, Predicate shouldRunPredicate) { + ImmutableSet.copyOf(descriptor.getChildren()) + .forEach(child -> removeNonMatching(child, shouldRunPredicate)); + + if (!descriptor.isRoot() && descriptor.getChildren().isEmpty() && !shouldRunPredicate.test(descriptor)) { + descriptor.removeFromHierarchy(); + } + } + + private static boolean memberNameMatches(TestDescriptor testDescriptor, List memberNames) { + UniqueId.Segment lastSegment = testDescriptor.getUniqueId().getLastSegment(); + return MEMBER_SEGMENT_TYPES.contains(lastSegment.getType()) + && memberNames.stream().anyMatch(it -> lastSegment.getValue().equals(it)); + } +} diff --git a/archunit-junit/junit6/engine/src/main/java/com/tngtech/archunit/junit/internal/ArchUnitTestDescriptor.java b/archunit-junit/junit6/engine/src/main/java/com/tngtech/archunit/junit/internal/ArchUnitTestDescriptor.java new file mode 100644 index 0000000000..1ef59adacd --- /dev/null +++ b/archunit-junit/junit6/engine/src/main/java/com/tngtech/archunit/junit/internal/ArchUnitTestDescriptor.java @@ -0,0 +1,342 @@ +/* + * Copyright 2014-2026 TNG Technology Consulting GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.tngtech.archunit.junit.internal; + +import java.lang.reflect.AccessibleObject; +import java.lang.reflect.Field; +import java.lang.reflect.Member; +import java.lang.reflect.Method; +import java.util.function.Consumer; +import java.util.function.Supplier; + +import com.tngtech.archunit.core.domain.JavaClasses; +import com.tngtech.archunit.core.importer.ImportOption; +import com.tngtech.archunit.junit.AnalyzeClasses; +import com.tngtech.archunit.junit.ArchTest; +import com.tngtech.archunit.junit.ArchTests; +import com.tngtech.archunit.junit.CacheMode; +import com.tngtech.archunit.junit.LocationProvider; +import com.tngtech.archunit.junit.engine_api.FieldSource; +import com.tngtech.archunit.lang.ArchRule; +import org.junit.platform.engine.TestDescriptor; +import org.junit.platform.engine.TestSource; +import org.junit.platform.engine.UniqueId; +import org.junit.platform.engine.support.descriptor.ClassSource; +import org.junit.platform.engine.support.descriptor.MethodSource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static com.tngtech.archunit.junit.internal.DisplayNameResolver.determineDisplayName; +import static com.tngtech.archunit.junit.internal.ReflectionUtils.findAnnotation; +import static com.tngtech.archunit.junit.internal.ReflectionUtils.getAllFields; +import static com.tngtech.archunit.junit.internal.ReflectionUtils.getAllMethods; +import static com.tngtech.archunit.junit.internal.ReflectionUtils.getValueOrThrowException; +import static com.tngtech.archunit.junit.internal.ReflectionUtils.invokeMethod; +import static com.tngtech.archunit.junit.internal.ReflectionUtils.tryFindAnnotation; +import static com.tngtech.archunit.junit.internal.ReflectionUtils.withAnnotation; + +class ArchUnitTestDescriptor extends AbstractArchUnitTestDescriptor implements CreatesChildren { + private static final Logger LOG = LoggerFactory.getLogger(ArchUnitTestDescriptor.class); + + static final String CLASS_SEGMENT_TYPE = "class"; + static final String FIELD_SEGMENT_TYPE = "field"; + static final String METHOD_SEGMENT_TYPE = "method"; + + private final Class testClass; + @SuppressWarnings("FieldMayBeFinal") // We want to change this in tests + private ClassCache classCache; + + private ArchUnitTestDescriptor(ElementResolver resolver, Class testClass, ClassCache classCache) { + super(resolver.getUniqueId(), testClass.getSimpleName(), ClassSource.from(testClass), testClass); + this.testClass = testClass; + this.classCache = classCache; + } + + static void resolve(TestDescriptor parent, ElementResolver resolver, ClassCache classCache) { + resolver.resolveClass() + .ifRequestedAndResolved(CreatesChildren::createChildren) + .ifRequestedButUnresolved((clazz, childResolver) -> createTestDescriptor(parent, classCache, clazz, childResolver)); + } + + private static void createTestDescriptor(TestDescriptor parent, ClassCache classCache, Class clazz, ElementResolver childResolver) { + if (!tryFindAnnotation(clazz, AnalyzeClasses.class).isPresent()) { + LOG.warn("Class {} is not (meta-)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(), + clazz.getSimpleName(), ArchTests.class.getSimpleName(), clazz.getSimpleName()); + + return; + } + + ArchUnitTestDescriptor classDescriptor = new ArchUnitTestDescriptor(childResolver, clazz, classCache); + parent.addChild(classDescriptor); + classDescriptor.createChildren(childResolver); + } + + @Override + public void createChildren(ElementResolver resolver) { + Supplier classes = () -> classCache.getClassesToAnalyzeFor(testClass, new JUnit6ClassAnalysisRequest(testClass)); + + getAllFields(testClass, withAnnotation(ArchTest.class)) + .forEach(field -> resolveField(resolver, classes, new TestMember<>(testClass, field))); + getAllMethods(testClass, withAnnotation(ArchTest.class)) + .forEach(method -> resolveMethod(resolver, classes, new TestMember<>(testClass, method))); + } + + private void resolveField(ElementResolver resolver, Supplier classes, TestMember field) { + resolver.resolveField(field.member) + .ifUnresolved(childResolver -> resolveChildren(this, childResolver, field, classes)); + } + + private void resolveMethod(ElementResolver resolver, Supplier classes, TestMember method) { + resolver.resolveMethod(method.member) + .ifUnresolved(childResolver -> addChild(new ArchUnitMethodDescriptor(getUniqueId(), method, classes))); + } + + private static void resolveChildren( + TestDescriptor parent, ElementResolver resolver, TestMember field, Supplier classes) { + + if (ArchTests.class.isAssignableFrom(field.member.getType())) { + resolveArchRules(parent, resolver, field, classes); + } else { + parent.addChild(new ArchUnitRuleDescriptor(resolver.getUniqueId(), getValue(field), classes, field)); + } + } + + private static T getValue(TestMember field) { + return getValueOrThrowException(field.member, field.owner, ArchTestInitializationException::new); + } + + private static void resolveArchRules( + TestDescriptor parent, ElementResolver resolver, TestMember field, Supplier classes) { + + DeclaredArchTests archTests = getDeclaredArchTests(field); + + resolver.resolveClass(archTests.getDefinitionLocation()) + .ifRequestedAndResolved(CreatesChildren::createChildren) + .ifRequestedButUnresolved((clazz, childResolver) -> { + ArchUnitArchTestsDescriptor rulesDescriptor = new ArchUnitArchTestsDescriptor(childResolver, archTests, classes, field); + parent.addChild(rulesDescriptor); + rulesDescriptor.createChildren(childResolver); + }); + } + + private static DeclaredArchTests getDeclaredArchTests(TestMember field) { + return new DeclaredArchTests(getValue(field)); + } + + @Override + public Type getType() { + return Type.CONTAINER; + } + + @Override + public void after(ArchUnitEngineExecutionContext context) { + classCache.clear(testClass); + } + + private static class ArchUnitRuleDescriptor extends AbstractArchUnitTestDescriptor { + private final ArchRule rule; + private final Supplier classes; + + ArchUnitRuleDescriptor(UniqueId uniqueId, ArchRule rule, Supplier classes, TestMember field) { + super(uniqueId, determineDisplayName(formatWithPath(uniqueId, field.getName())), FieldSource.from(field.member), field.member); + this.rule = rule; + this.classes = classes; + } + + @Override + public Type getType() { + return Type.TEST; + } + + @Override + public ArchUnitEngineExecutionContext execute(ArchUnitEngineExecutionContext context, DynamicTestExecutor dynamicTestExecutor) { + rule.check(classes.get()); + return context; + } + } + + private static class ArchUnitMethodDescriptor extends AbstractArchUnitTestDescriptor { + private final TestMember method; + private final Supplier classes; + + ArchUnitMethodDescriptor(UniqueId uniqueId, TestMember method, Supplier classes) { + super(uniqueId.append("method", method.member.getName()), + determineDisplayName(formatWithPath(uniqueId, method.member.getName())), + MethodSource.from(method.member), + method.member); + + validate(method.member); + + this.method = method; + this.classes = classes; + } + + private void validate(Method method) { + ArchTestInitializationException.check( + method.getParameterCount() == 1 && method.getParameterTypes()[0].equals(JavaClasses.class), + "@%s Method %s.%s must have exactly one parameter of type %s", + ArchTest.class.getSimpleName(), method.getDeclaringClass().getSimpleName(), method.getName(), JavaClasses.class.getName()); + } + + @Override + public Type getType() { + return Type.TEST; + } + + @Override + public ArchUnitEngineExecutionContext execute(ArchUnitEngineExecutionContext context, DynamicTestExecutor dynamicTestExecutor) { + invokeMethod(method.member, method.owner, classes.get()); + return context; + } + } + + private static class ArchUnitArchTestsDescriptor extends AbstractArchUnitTestDescriptor implements CreatesChildren { + private final DeclaredArchTests archTests; + private final Supplier classes; + + ArchUnitArchTestsDescriptor(ElementResolver resolver, DeclaredArchTests archTests, Supplier classes, TestMember field) { + + super(resolver.getUniqueId(), + archTests.getDisplayName(), + noSource(), + field.member, + archTests.getDefinitionLocation()); + this.archTests = archTests; + this.classes = classes; + } + + /** + * We don't pass a ClassSource for intermediary descriptors or it will be used as test location by test executors. + * We want the root class declaring @AnalyzeClasses to be used for this. + */ + private static TestSource noSource() { + return null; + } + + @Override + public void createChildren(ElementResolver resolver) { + archTests.handleFields(field -> + resolver.resolve( + FIELD_SEGMENT_TYPE, + field.getName(), + childResolver -> resolveChildren(field, childResolver))); + + archTests.handleMethods(method -> + resolver.resolve( + METHOD_SEGMENT_TYPE, + method.getName(), + childResolver -> addChild(method))); + } + + private void resolveChildren(Field field, ElementResolver childResolver) { + ArchUnitTestDescriptor.resolveChildren( + this, + childResolver, + new TestMember<>(archTests.getDefinitionLocation(), field), + classes); + } + + private void addChild(Method method) { + addChild(new ArchUnitMethodDescriptor( + getUniqueId(), + new TestMember<>(archTests.getDefinitionLocation(), method), + classes)); + } + + @Override + public Type getType() { + return Type.CONTAINER; + } + } + + private static class DeclaredArchTests { + private final ArchTests archTests; + + DeclaredArchTests(ArchTests archTests) { + this.archTests = archTests; + } + + Class getDefinitionLocation() { + return archTests.getDefinitionLocation(); + } + + String getDisplayName() { + return archTests.getDefinitionLocation().getSimpleName(); + } + + void handleFields(Consumer doWithField) { + getAllFields(archTests.getDefinitionLocation(), withAnnotation(ArchTest.class)).forEach(doWithField); + } + + void handleMethods(Consumer doWithMethod) { + getAllMethods(archTests.getDefinitionLocation(), withAnnotation(ArchTest.class)).forEach(doWithMethod); + } + } + + private static class JUnit6ClassAnalysisRequest implements ClassAnalysisRequest { + private final AnalyzeClasses analyzeClasses; + + JUnit6ClassAnalysisRequest(Class testClass) { + analyzeClasses = findAnnotation(testClass, AnalyzeClasses.class); + } + + @Override + public String[] getPackageNames() { + return analyzeClasses.packages(); + } + + @Override + public Class[] getPackageRoots() { + return analyzeClasses.packagesOf(); + } + + @Override + public Class[] getLocationProviders() { + return analyzeClasses.locations(); + } + + @Override + public Class[] getImportOptions() { + return analyzeClasses.importOptions(); + } + + @Override + public CacheMode getCacheMode() { + return analyzeClasses.cacheMode(); + } + + @Override + public boolean scanWholeClasspath() { + return analyzeClasses.wholeClasspath(); + } + } + + private static class TestMember { + final Class owner; + final MEMBER member; + + TestMember(Class owner, MEMBER member) { + this.owner = owner; + this.member = member; + } + + String getName() { + return member.getName(); + } + } +} diff --git a/archunit-junit/junit6/engine/src/main/java/com/tngtech/archunit/junit/internal/ArchUnitTestEngine.java b/archunit-junit/junit6/engine/src/main/java/com/tngtech/archunit/junit/internal/ArchUnitTestEngine.java new file mode 100644 index 0000000000..b48965167f --- /dev/null +++ b/archunit-junit/junit6/engine/src/main/java/com/tngtech/archunit/junit/internal/ArchUnitTestEngine.java @@ -0,0 +1,236 @@ +/* + * Copyright 2014-2026 TNG Technology Consulting GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.tngtech.archunit.junit.internal; + +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URL; +import java.util.List; +import java.util.Optional; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Stream; + +import com.tngtech.archunit.ArchConfiguration; +import com.tngtech.archunit.Internal; +import com.tngtech.archunit.base.MayResolveTypesViaReflection; +import com.tngtech.archunit.core.domain.JavaClass; +import com.tngtech.archunit.core.domain.JavaClasses; +import com.tngtech.archunit.core.importer.ClassFileImporter; +import com.tngtech.archunit.core.importer.resolvers.ClassResolver; +import com.tngtech.archunit.junit.AnalyzeClasses; +import com.tngtech.archunit.junit.ArchTest; +import com.tngtech.archunit.junit.engine_api.FieldSelector; +import org.junit.platform.engine.EngineDiscoveryRequest; +import org.junit.platform.engine.ExecutionRequest; +import org.junit.platform.engine.Filter; +import org.junit.platform.engine.TestDescriptor; +import org.junit.platform.engine.UniqueId; +import org.junit.platform.engine.discovery.ClassNameFilter; +import org.junit.platform.engine.discovery.ClassSelector; +import org.junit.platform.engine.discovery.ClasspathRootSelector; +import org.junit.platform.engine.discovery.MethodSelector; +import org.junit.platform.engine.discovery.PackageNameFilter; +import org.junit.platform.engine.discovery.PackageSelector; +import org.junit.platform.engine.discovery.UniqueIdSelector; +import org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine; + +import static com.tngtech.archunit.junit.internal.ReflectionUtils.getAllFields; +import static com.tngtech.archunit.junit.internal.ReflectionUtils.getAllMethods; +import static com.tngtech.archunit.junit.internal.ReflectionUtils.withAnnotation; +import static java.util.stream.Collectors.toList; + +/** + * A simple test engine to discover and execute ArchUnit tests with JUnit 6. In particular the engine + * uses a {@link ClassCache} to avoid the costly import process as much as possible. + *

+ * Mark classes to be executed by the {@link ArchUnitTestEngine} with {@link AnalyzeClasses @AnalyzeClasses} and + * rule fields or methods with {@link ArchTest @ArchTest}. Example: + *

+ *{@literal @}AnalyzeClasses(packages = "com.foo")
+ * class MyArchTest {
+ *    {@literal @}ArchTest
+ *     public static final ArchRule myRule = classes()...
+ * }
+ * 
+ */ +@Internal +public final class ArchUnitTestEngine extends HierarchicalTestEngine { + static final String UNIQUE_ID = "archunit"; + + private final ArchUnitSystemPropertyTestFilterJUnit6 systemPropertyTestFilter = new ArchUnitSystemPropertyTestFilterJUnit6(); + + @SuppressWarnings("FieldMayBeFinal") + private SharedCache cache = new SharedCache(); // NOTE: We want to change this in tests -> no static/final reference + + @Override + public String getId() { + return UNIQUE_ID; + } + + @Override + public TestDescriptor discover(EngineDiscoveryRequest discoveryRequest, UniqueId uniqueId) { + ArchUnitEngineDescriptor result = new ArchUnitEngineDescriptor(uniqueId); + + resolveRequestedClasspathRoot(discoveryRequest, uniqueId, result); + resolveRequestedPackages(discoveryRequest, uniqueId, result); + resolveRequestedClasses(discoveryRequest, uniqueId, result); + resolveRequestedMethods(discoveryRequest, uniqueId, result); + resolveRequestedFields(discoveryRequest, uniqueId, result); + resolveRequestedUniqueIds(discoveryRequest, uniqueId, result); + + systemPropertyTestFilter.filter(result); + + return result; + } + + private void resolveRequestedClasspathRoot(EngineDiscoveryRequest discoveryRequest, UniqueId uniqueId, ArchUnitEngineDescriptor result) { + Stream classes = discoveryRequest.getSelectorsByType(ClasspathRootSelector.class).stream() + .flatMap(this::getContainedClasses); + filterCandidatesAndLoadClasses(classes, discoveryRequest) + .forEach(clazz -> ArchUnitTestDescriptor.resolve( + result, ElementResolver.create(result, uniqueId, clazz), cache.get())); + } + + private void resolveRequestedPackages(EngineDiscoveryRequest discoveryRequest, UniqueId uniqueId, ArchUnitEngineDescriptor result) { + String[] packages = discoveryRequest.getSelectorsByType(PackageSelector.class).stream() + .map(PackageSelector::getPackageName) + .toArray(String[]::new); + Stream classes = getContainedClasses(packages); + + filterCandidatesAndLoadClasses(classes, discoveryRequest) + .forEach(clazz -> ArchUnitTestDescriptor.resolve( + result, ElementResolver.create(result, uniqueId, clazz), cache.get())); + } + + private Stream> filterCandidatesAndLoadClasses(Stream classes, EngineDiscoveryRequest discoveryRequest) { + return classes + .filter(isAllowedBy(discoveryRequest)) + .filter(this::isArchUnitTestCandidate) + .flatMap(this::safelyReflect); + } + + private void resolveRequestedClasses(EngineDiscoveryRequest discoveryRequest, UniqueId uniqueId, ArchUnitEngineDescriptor result) { + discoveryRequest.getSelectorsByType(ClassSelector.class).stream() + .map(ClassSelector::getJavaClass) + .filter(this::isArchUnitTestCandidate) + .forEach(clazz -> ArchUnitTestDescriptor.resolve( + result, ElementResolver.create(result, uniqueId, clazz), cache.get())); + } + + private void resolveRequestedMethods(EngineDiscoveryRequest discoveryRequest, UniqueId uniqueId, ArchUnitEngineDescriptor result) { + discoveryRequest.getSelectorsByType(MethodSelector.class).stream() + .filter(s -> s.getJavaMethod().isAnnotationPresent(ArchTest.class)) + .forEach(selector -> ArchUnitTestDescriptor.resolve( + result, ElementResolver.create(result, uniqueId, selector.getJavaClass(), selector.getJavaMethod()), cache.get())); + } + + private void resolveRequestedFields(EngineDiscoveryRequest discoveryRequest, UniqueId uniqueId, ArchUnitEngineDescriptor result) { + discoveryRequest.getSelectorsByType(FieldSelector.class).stream() + .filter(s -> s.getJavaField().isAnnotationPresent(ArchTest.class)) + .forEach(selector -> ArchUnitTestDescriptor.resolve( + result, ElementResolver.create(result, uniqueId, selector.getJavaClass(), selector.getJavaField()), cache.get())); + } + + private void resolveRequestedUniqueIds(EngineDiscoveryRequest discoveryRequest, UniqueId uniqueId, ArchUnitEngineDescriptor result) { + discoveryRequest.getSelectorsByType(UniqueIdSelector.class).stream() + .filter(selector -> selector.getUniqueId().getEngineId().equals(Optional.of(getId()))) + .forEach(selector -> ArchUnitTestDescriptor.resolve( + result, ElementResolver.create(result, uniqueId, selector.getUniqueId()), cache.get())); + } + + private Stream getContainedClasses(String[] packages) { + if (packages.length == 0) { + return Stream.empty(); + } + return discoverClasses(importer -> importer.importPackages(packages)); + } + + private Stream getContainedClasses(ClasspathRootSelector selector) { + return discoverClasses(importer -> importer.importUrl(toUrl(selector.getClasspathRoot()))); + } + + private Stream discoverClasses(Function importClasses) { + return ArchConfiguration.withThreadLocalScope(configuration -> { + configuration.setClassResolver(NoOpClassResolver.class); + return importClasses.apply(new ClassFileImporter()).stream(); + }); + } + + private Predicate isAllowedBy(EngineDiscoveryRequest discoveryRequest) { + List> filters = Stream + .concat(discoveryRequest.getFiltersByType(ClassNameFilter.class).stream(), + discoveryRequest.getFiltersByType(PackageNameFilter.class).stream()) + .map(Filter::toPredicate) + .collect(toList()); + + return javaClass -> filters.stream().allMatch(p -> p.test(javaClass.getName())); + } + + private boolean isArchUnitTestCandidate(JavaClass javaClass) { + return javaClass.getAllMembers().stream().anyMatch(m -> m.isAnnotatedWith(ArchTest.class)); + } + + @MayResolveTypesViaReflection(reason = "Within the ArchUnitTestEngine we may resolve types via reflection, since they are needed anyway") + private Stream> safelyReflect(JavaClass javaClass) { + try { + return Stream.of(javaClass.reflect()); + } catch (NoClassDefFoundError | RuntimeException e) { + return Stream.empty(); + } + } + + private boolean isArchUnitTestCandidate(Class clazz) { + try { + return !getAllFields(clazz, withAnnotation(ArchTest.class)).isEmpty() + || !getAllMethods(clazz, withAnnotation(ArchTest.class)).isEmpty(); + } catch (NoClassDefFoundError | Exception e) { + return false; + } + } + + private URL toUrl(URI uri) { + try { + return uri.toURL(); + } catch (MalformedURLException e) { + throw new ArchTestInitializationException(e); + } + } + + @Override + protected ArchUnitEngineExecutionContext createExecutionContext(ExecutionRequest request) { + return new ArchUnitEngineExecutionContext(); + } + + static class SharedCache { + private static final ClassCache cache = new ClassCache(); + + ClassCache get() { + return cache; + } + } + + static class NoOpClassResolver implements ClassResolver { + @Override + public void setClassUriImporter(ClassUriImporter classUriImporter) { + } + + @Override + public Optional tryResolve(String typeName) { + return Optional.empty(); + } + } +} diff --git a/archunit-junit/junit6/engine/src/main/java/com/tngtech/archunit/junit/internal/CreatesChildren.java b/archunit-junit/junit6/engine/src/main/java/com/tngtech/archunit/junit/internal/CreatesChildren.java new file mode 100644 index 0000000000..650b7f7600 --- /dev/null +++ b/archunit-junit/junit6/engine/src/main/java/com/tngtech/archunit/junit/internal/CreatesChildren.java @@ -0,0 +1,20 @@ +/* + * Copyright 2014-2026 TNG Technology Consulting GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.tngtech.archunit.junit.internal; + +interface CreatesChildren { + void createChildren(ElementResolver resolver); +} diff --git a/archunit-junit/junit6/engine/src/main/java/com/tngtech/archunit/junit/internal/ElementResolver.java b/archunit-junit/junit6/engine/src/main/java/com/tngtech/archunit/junit/internal/ElementResolver.java new file mode 100644 index 0000000000..9ac9a85d77 --- /dev/null +++ b/archunit-junit/junit6/engine/src/main/java/com/tngtech/archunit/junit/internal/ElementResolver.java @@ -0,0 +1,233 @@ +/* + * Copyright 2014-2026 TNG Technology Consulting GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.tngtech.archunit.junit.internal; + +import java.lang.reflect.Field; +import java.lang.reflect.Member; +import java.lang.reflect.Method; +import java.util.Deque; +import java.util.LinkedList; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Predicate; + +import com.tngtech.archunit.base.ClassLoaders; +import com.tngtech.archunit.base.MayResolveTypesViaReflection; +import org.junit.platform.engine.TestDescriptor; +import org.junit.platform.engine.UniqueId; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; +import static com.tngtech.archunit.junit.internal.ArchUnitTestDescriptor.CLASS_SEGMENT_TYPE; +import static com.tngtech.archunit.junit.internal.ArchUnitTestDescriptor.FIELD_SEGMENT_TYPE; +import static com.tngtech.archunit.junit.internal.ArchUnitTestDescriptor.METHOD_SEGMENT_TYPE; + +class ElementResolver { + private final ArchUnitEngineDescriptor engineDescriptor; + private final UniqueId processedId; + private final Deque segmentsToResolve; + + private ElementResolver(ArchUnitEngineDescriptor engineDescriptor, UniqueId processedId, Deque segmentsToResolve) { + this.engineDescriptor = checkNotNull(engineDescriptor); + this.processedId = checkNotNull(processedId); + this.segmentsToResolve = checkNotNull(segmentsToResolve); + } + + PossiblyResolvedClass resolveClass() { + UniqueId.Segment nextSegment = checkNotNull(segmentsToResolve.peekFirst()); + if (!CLASS_SEGMENT_TYPE.equals(nextSegment.getType())) { + return new ClassNotRequested(); + } + + return tryResolveClass(classOf(nextSegment), processedId.append(nextSegment)); + } + + PossiblyResolvedClass resolveClass(Class clazz) { + return tryResolveClass(clazz, processedId.append(CLASS_SEGMENT_TYPE, clazz.getName())); + } + + PossiblyResolvedMember resolveMethod(Method method) { + return resolveMember(method, METHOD_SEGMENT_TYPE); + } + + PossiblyResolvedMember resolveField(Field field) { + return resolveMember(field, FIELD_SEGMENT_TYPE); + } + + private PossiblyResolvedMember resolveMember(Member member, String segmentType) { + UniqueId requestedId = processedId.append(segmentType, member.getName()); + return engineDescriptor.findByUniqueId(requestedId).isPresent() + ? new SuccessfullyResolvedMember() + : new UnresolvedMember(member, segmentType); + } + + void resolve(String segmentType, String segmentValue, Consumer doIfResolved) { + if (segmentsToResolve.isEmpty()) { + handleNewSegment(segmentType, segmentValue, doIfResolved); + } else { + handleRequestedSegment(segmentType, segmentValue, doIfResolved); + } + } + + UniqueId getUniqueId() { + return processedId; + } + + private PossiblyResolvedClass tryResolveClass(Class clazz, UniqueId classId) { + ElementResolver childResolver = new ElementResolver(engineDescriptor, classId, tail(segmentsToResolve)); + return engineDescriptor.findByUniqueId(classId) + .map(testDescriptor -> + new RequestedAndSuccessfullyResolvedClass(testDescriptor, childResolver)) + .orElseGet(() -> new RequestedButUnresolvedClass(clazz, childResolver)); + } + + private Deque tail(Deque segmentsToResolve) { + LinkedList result = new LinkedList<>(segmentsToResolve); + result.pollFirst(); + return result; + } + + @MayResolveTypesViaReflection(reason = "Within the ArchUnitTestEngine we may resolve types via reflection, since they are needed anyway") + private Class classOf(UniqueId.Segment segment) { + try { + return ClassLoaders.loadClass(segment.getValue()); + } catch (ClassNotFoundException e) { + throw new ArchTestInitializationException(e, "Failed to load class from %s segment %s", + UniqueId.class.getSimpleName(), segment); + } + } + + private void handleRequestedSegment(String segmentType, String segmentValue, Consumer doIfResolved) { + UniqueId.Segment nextSegment = checkNotNull(segmentsToResolve.peekFirst()); + if (matches(segmentType, segmentValue).test(nextSegment)) { + doIfResolved.accept(new ElementResolver(engineDescriptor, processedId.append(nextSegment), tail(segmentsToResolve))); + } + } + + private Predicate matches(String segmentType, String segmentValue) { + return nextSegment -> nextSegment.getType().equals(segmentType) && nextSegment.getValue().equals(segmentValue); + } + + private void handleNewSegment(String segmentType, String segmentValue, Consumer doIfResolved) { + doIfResolved.accept(new ElementResolver(engineDescriptor, processedId.append(segmentType, segmentValue), new LinkedList<>())); + } + + static ElementResolver create(ArchUnitEngineDescriptor engineDescriptor, UniqueId rootId, UniqueId targetId) { + return new ElementResolver(engineDescriptor, rootId, getRemainingSegments(rootId, targetId)); + } + + static ElementResolver create(ArchUnitEngineDescriptor engineDescriptor, UniqueId rootId, Class testClass) { + UniqueId targetId = rootId.append(CLASS_SEGMENT_TYPE, testClass.getName()); + return create(engineDescriptor, rootId, targetId); + } + + static ElementResolver create(ArchUnitEngineDescriptor engineDescriptor, UniqueId rootId, Class testClass, Method testMethod) { + UniqueId targetId = rootId + .append(CLASS_SEGMENT_TYPE, testClass.getName()) + .append(METHOD_SEGMENT_TYPE, testMethod.getName()); + return create(engineDescriptor, rootId, targetId); + } + + static ElementResolver create(ArchUnitEngineDescriptor engineDescriptor, UniqueId rootId, Class testClass, Field testField) { + UniqueId targetId = rootId + .append(CLASS_SEGMENT_TYPE, testClass.getName()) + .append(FIELD_SEGMENT_TYPE, testField.getName()); + return create(engineDescriptor, rootId, targetId); + } + + private static Deque getRemainingSegments(UniqueId rootId, UniqueId targetId) { + Deque remainingSegments = new LinkedList<>(targetId.getSegments()); + rootId.getSegments().forEach(segment -> { + checkState(segment.equals(remainingSegments.peekFirst()), + "targetId %s should start with rootId %s", targetId, rootId); + remainingSegments.pollFirst(); + }); + return remainingSegments; + } + + abstract static class PossiblyResolvedClass { + void ifRequestedButUnresolved(BiConsumer, ElementResolver> doIfResolved) { + } + + PossiblyResolvedClass ifRequestedAndResolved(BiConsumer doIfResolved) { + return this; + } + } + + private static class RequestedAndSuccessfullyResolvedClass extends PossiblyResolvedClass { + private final CreatesChildren classDescriptor; + private final ElementResolver childResolver; + + RequestedAndSuccessfullyResolvedClass(TestDescriptor classDescriptor, ElementResolver childResolver) { + checkArgument(classDescriptor instanceof CreatesChildren, + "descriptor with uniqueId %s is expected to implement %s", + classDescriptor.getUniqueId(), CreatesChildren.class.getSimpleName()); + + this.classDescriptor = (CreatesChildren) classDescriptor; + this.childResolver = childResolver; + } + + @Override + RequestedAndSuccessfullyResolvedClass ifRequestedAndResolved(BiConsumer doIfResolved) { + doIfResolved.accept(classDescriptor, childResolver); + return this; + } + } + + private class RequestedButUnresolvedClass extends PossiblyResolvedClass { + private final Class clazz; + private final ElementResolver childResolver; + + RequestedButUnresolvedClass(Class clazz, ElementResolver childResolver) { + this.clazz = clazz; + this.childResolver = childResolver; + } + + @Override + void ifRequestedButUnresolved(BiConsumer, ElementResolver> doWithChildResolver) { + doWithChildResolver.accept(clazz, childResolver); + } + } + + private static class ClassNotRequested extends PossiblyResolvedClass { + } + + abstract static class PossiblyResolvedMember { + abstract void ifUnresolved(Consumer childResolver); + } + + private static class SuccessfullyResolvedMember extends PossiblyResolvedMember { + @Override + void ifUnresolved(Consumer childResolver) { + } + } + + private class UnresolvedMember extends PossiblyResolvedMember { + private final Member member; + private final String segmentType; + + UnresolvedMember(Member member, String segmentType) { + this.member = member; + this.segmentType = segmentType; + } + + @Override + void ifUnresolved(Consumer doWithChildResolver) { + resolve(segmentType, member.getName(), doWithChildResolver); + } + } +} diff --git a/archunit-junit/junit6/engine/src/main/resources/META-INF/services/org.junit.platform.engine.TestEngine b/archunit-junit/junit6/engine/src/main/resources/META-INF/services/org.junit.platform.engine.TestEngine new file mode 100644 index 0000000000..65a1f817b8 --- /dev/null +++ b/archunit-junit/junit6/engine/src/main/resources/META-INF/services/org.junit.platform.engine.TestEngine @@ -0,0 +1 @@ +com.tngtech.archunit.junit.internal.ArchUnitTestEngine diff --git a/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/ArchUnitTestEngineTest.java b/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/ArchUnitTestEngineTest.java new file mode 100644 index 0000000000..449c2f3269 --- /dev/null +++ b/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/ArchUnitTestEngineTest.java @@ -0,0 +1,1510 @@ +package com.tngtech.archunit.junit.internal; + +import java.net.URI; +import java.net.URL; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Stream; + +import com.google.common.collect.ImmutableSet; +import com.tngtech.archunit.ArchConfiguration; +import com.tngtech.archunit.core.domain.JavaClasses; +import com.tngtech.archunit.core.importer.ClassFileImporter; +import com.tngtech.archunit.junit.AnalyzeClasses; +import com.tngtech.archunit.junit.ArchTest; +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.ClassWithPrivateTests; +import com.tngtech.archunit.junit.internal.testexamples.ComplexMetaTags; +import com.tngtech.archunit.junit.internal.testexamples.ComplexRuleLibrary; +import com.tngtech.archunit.junit.internal.testexamples.ComplexTags; +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; +import com.tngtech.archunit.junit.internal.testexamples.TestFieldWithMetaTag; +import com.tngtech.archunit.junit.internal.testexamples.TestFieldWithMetaTags; +import com.tngtech.archunit.junit.internal.testexamples.TestFieldWithTags; +import com.tngtech.archunit.junit.internal.testexamples.TestMethodWithMetaTag; +import com.tngtech.archunit.junit.internal.testexamples.TestMethodWithMetaTags; +import com.tngtech.archunit.junit.internal.testexamples.TestMethodWithTags; +import com.tngtech.archunit.junit.internal.testexamples.UnwantedClass; +import com.tngtech.archunit.junit.internal.testexamples.abstractbase.ArchTestWithAbstractBaseClassWithFieldRule; +import com.tngtech.archunit.junit.internal.testexamples.abstractbase.ArchTestWithAbstractBaseClassWithMethodRule; +import com.tngtech.archunit.junit.internal.testexamples.abstractbase.ArchTestWithLibraryWithAbstractBaseClass; +import com.tngtech.archunit.junit.internal.testexamples.ignores.IgnoredClass; +import com.tngtech.archunit.junit.internal.testexamples.ignores.IgnoredField; +import com.tngtech.archunit.junit.internal.testexamples.ignores.IgnoredLibrary; +import com.tngtech.archunit.junit.internal.testexamples.ignores.IgnoredMethod; +import com.tngtech.archunit.junit.internal.testexamples.ignores.MetaIgnoredClass; +import com.tngtech.archunit.junit.internal.testexamples.ignores.MetaIgnoredField; +import com.tngtech.archunit.junit.internal.testexamples.ignores.MetaIgnoredLibrary; +import com.tngtech.archunit.junit.internal.testexamples.ignores.MetaIgnoredMethod; +import com.tngtech.archunit.junit.internal.testexamples.subone.SimpleRuleField; +import com.tngtech.archunit.junit.internal.testexamples.subone.SimpleRuleMethod; +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.testutil.LogCaptor; +import com.tngtech.archunit.junit.internal.testutil.SystemPropertiesExtension; +import com.tngtech.archunit.junit.internal.testutil.TestLogExtension; +import com.tngtech.archunit.testutil.TestLogRecorder.RecordedLogEvent; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.platform.engine.ExecutionRequest; +import org.junit.platform.engine.TestDescriptor; +import org.junit.platform.engine.TestTag; +import org.junit.platform.engine.UniqueId; +import org.junit.platform.engine.discovery.ClasspathRootSelector; +import org.junit.platform.engine.discovery.MethodSelector; +import org.junit.platform.engine.discovery.PackageSelector; +import org.junit.platform.engine.support.descriptor.ClassSource; +import org.junit.platform.engine.support.descriptor.MethodSource; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; + +import static com.google.common.collect.Iterables.getLast; +import static com.google.common.collect.Iterables.getOnlyElement; +import static com.tngtech.archunit.core.domain.TestUtils.importClasses; +import static com.tngtech.archunit.junit.internal.ArchUnitTestDescriptor.CLASS_SEGMENT_TYPE; +import static com.tngtech.archunit.junit.internal.ArchUnitTestDescriptor.FIELD_SEGMENT_TYPE; +import static com.tngtech.archunit.junit.internal.ArchUnitTestDescriptor.METHOD_SEGMENT_TYPE; +import static com.tngtech.archunit.junit.internal.EngineExecutionTestListener.onlyElement; +import static com.tngtech.archunit.junit.internal.testexamples.TestFieldWithMetaTag.FIELD_WITH_META_TAG_NAME; +import static com.tngtech.archunit.junit.internal.testexamples.TestFieldWithMetaTags.FIELD_WITH_META_TAGS_NAME; +import static com.tngtech.archunit.junit.internal.testexamples.TestFieldWithTags.FIELD_WITH_TAG_NAME; +import static com.tngtech.archunit.junit.internal.testexamples.TestMethodWithMetaTag.METHOD_WITH_META_TAG_NAME; +import static com.tngtech.archunit.junit.internal.testexamples.TestMethodWithMetaTags.METHOD_WITH_META_TAGS_NAME; +import static com.tngtech.archunit.junit.internal.testexamples.TestMethodWithTags.METHOD_WITH_TAG_NAME; +import static com.tngtech.archunit.junit.internal.testexamples.subone.SimpleRuleField.SIMPLE_RULE_FIELD_NAME; +import static com.tngtech.archunit.junit.internal.testexamples.subone.SimpleRuleMethod.SIMPLE_RULE_METHOD_NAME; +import static com.tngtech.archunit.testutil.ReflectionTestUtils.field; +import static java.util.Collections.singleton; +import static java.util.function.Function.identity; +import static java.util.stream.Collectors.toList; +import static java.util.stream.Collectors.toSet; +import static org.apache.logging.log4j.Level.DEBUG; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.platform.engine.TestDescriptor.Type.CONTAINER; +import static org.junit.platform.engine.TestDescriptor.Type.TEST; +import static org.junit.platform.engine.discovery.ClassNameFilter.excludeClassNamePatterns; +import static org.junit.platform.engine.discovery.ClassNameFilter.includeClassNamePatterns; +import static org.junit.platform.engine.discovery.PackageNameFilter.excludePackageNames; +import static org.junit.platform.engine.discovery.PackageNameFilter.includePackageNames; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +@ExtendWith({MockitoExtension.class, SystemPropertiesExtension.class}) +@MockitoSettings(strictness = Strictness.WARN) +class ArchUnitTestEngineTest { + @Mock + private ClassCache classCache; + @Mock + private SharedCache sharedCache; + @Captor + private ArgumentCaptor classAnalysisRequestCaptor; + + @InjectMocks + private ArchUnitTestEngine testEngine; + + private final UniqueId engineId = createEngineId(); + + @BeforeEach + void setUp() { + when(sharedCache.get()).thenReturn(classCache); + } + + @Nested + class Discovers { + @Test + void a_root_that_is_a_test_container() { + TestDescriptor descriptor = testEngine.discover(new EngineDiscoveryTestRequest(), createEngineId()); + + assertThat(descriptor.getType().isContainer()).as("Root descriptor is container").isTrue(); + } + + @Test + void a_root_with_the_correct_unique_id() { + + TestDescriptor descriptor = testEngine.discover(new EngineDiscoveryTestRequest(), engineId); + + assertThat(descriptor.getUniqueId()).isEqualTo(engineId); + } + + @Test + void a_single_test_class() { + EngineDiscoveryTestRequest discoveryRequest = new EngineDiscoveryTestRequest().withClass(SimpleRuleField.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, SimpleRuleField.class.getName())); + assertThat(child.getDisplayName()).isEqualTo(SimpleRuleField.class.getSimpleName()); + assertThat(child.getType()).isEqualTo(CONTAINER); + assertThat(child.getParent().get()).isEqualTo(descriptor); + } + + @Test + void a_test_class_with_meta_annotated_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); + + TestDescriptor descriptor = testEngine.discover(discoveryRequest, createEngineId()); + + TestDescriptor child = getOnlyElement(descriptor.getChildren()); + + assertClassSource(child, SimpleRuleField.class); + } + + @Test + void multiple_test_classes() { + EngineDiscoveryTestRequest discoveryRequest = new EngineDiscoveryTestRequest() + .withClass(SimpleRuleField.class) + .withClass(SimpleRuleMethod.class); + + TestDescriptor descriptor = testEngine.discover(discoveryRequest, createEngineId()); + + Set displayNames = descriptor.getChildren().stream().map(TestDescriptor::getDisplayName).collect(toSet()); + assertThat(displayNames).containsOnly(SimpleRuleField.class.getSimpleName(), SimpleRuleMethod.class.getSimpleName()); + } + + @Test + void a_class_with_simple_rule_field() { + EngineDiscoveryTestRequest discoveryRequest = new EngineDiscoveryTestRequest().withClass(SimpleRuleField.class); + + TestDescriptor descriptor = testEngine.discover(discoveryRequest, engineId); + + TestDescriptor ruleDescriptor = getOnlyTest(descriptor); + + assertThat(ruleDescriptor.getUniqueId()).isEqualTo(simpleRuleFieldTestId(engineId)); + FieldSource testSource = ((FieldSource) ruleDescriptor.getSource().get()); + assertThat(testSource.getJavaClass()).isEqualTo(SimpleRuleField.class); + assertThat(testSource.getClassName()).isEqualTo(SimpleRuleField.class.getName()); + assertThat(testSource.getFieldName()).isEqualTo(SIMPLE_RULE_FIELD_NAME); + } + + @Test + void a_class_with_simple_rule_method() { + EngineDiscoveryTestRequest discoveryRequest = new EngineDiscoveryTestRequest().withClass(SimpleRuleMethod.class); + + TestDescriptor descriptor = testEngine.discover(discoveryRequest, engineId); + + TestDescriptor ruleDescriptor = getOnlyTest(descriptor); + + assertThat(ruleDescriptor.getUniqueId()).isEqualTo(simpleRuleMethodTestId(engineId)); + MethodSource testSource = (MethodSource) ruleDescriptor.getSource().get(); + assertThat(testSource.getClassName()).isEqualTo(SimpleRuleMethod.class.getName()); + assertThat(testSource.getMethodName()).isEqualTo(SIMPLE_RULE_METHOD_NAME); + assertThat(testSource.getMethodParameterTypes()).isEqualTo(JavaClasses.class.getName()); + } + + @Test + void a_class_with_simple_hierarchy__descriptor_types() { + EngineDiscoveryTestRequest discoveryRequest = new EngineDiscoveryTestRequest().withClass(SimpleRuleLibrary.class); + + TestDescriptor descriptor = testEngine.discover(discoveryRequest, engineId); + + Stream archRulesDescriptors = getArchRulesDescriptorsOfOnlyChild(descriptor); + boolean allAreContainer = archRulesDescriptors.allMatch(d -> d.getType().equals(CONTAINER)); + assertThat(allAreContainer).as("all rules descriptor have type " + CONTAINER).isTrue(); + } + + @Test + void a_class_with_simple_hierarchy__uniqueIds() { + EngineDiscoveryTestRequest discoveryRequest = new EngineDiscoveryTestRequest().withClass(SimpleRuleLibrary.class); + + TestDescriptor descriptor = testEngine.discover(discoveryRequest, engineId); + + Stream archRulesDescriptors = getArchRulesDescriptorsOfOnlyChild(descriptor); + + Set expectedIds = getExpectedIdsForSimpleRuleLibrary(engineId); + Set actualIds = archRulesDescriptors.flatMap(d -> d.getChildren().stream()) + .map(TestDescriptor::getUniqueId).collect(toSet()); + assertThat(actualIds).isEqualTo(expectedIds); + } + + @Test + void a_class_with_simple_hierarchy__class_source() { + EngineDiscoveryTestRequest discoveryRequest = new EngineDiscoveryTestRequest().withClass(SimpleRuleLibrary.class); + + TestDescriptor descriptor = testEngine.discover(discoveryRequest, engineId); + + assertClassSource(getOnlyElement(descriptor.getChildren()), SimpleRuleLibrary.class); + + List archRulesDescriptors = getArchRulesDescriptorsOfOnlyChild(descriptor).collect(toList()); + + TestDescriptor testDescriptor = findRulesDescriptor(archRulesDescriptors, SimpleRules.class); + assertNoIntermediaryTestSource(testDescriptor); + testDescriptor.getChildren().forEach(d -> + assertThat(d.getSource().isPresent()).as("source is present").isTrue()); + + testDescriptor = findRulesDescriptor(archRulesDescriptors, SimpleRuleField.class); + assertNoIntermediaryTestSource(testDescriptor); + testDescriptor.getChildren().forEach(d -> + assertThat(d.getSource().isPresent()).as("source is present").isTrue()); + } + + @Test + void a_class_with_simple_hierarchy__display_name() { + EngineDiscoveryTestRequest discoveryRequest = new EngineDiscoveryTestRequest().withClass(SimpleRuleLibrary.class); + + TestDescriptor descriptor = testEngine.discover(discoveryRequest, engineId); + + List archRulesDescriptors = getArchRulesDescriptorsOfOnlyChild(descriptor).collect(toList()); + + TestDescriptor testDescriptor = findRulesDescriptor(archRulesDescriptors, SimpleRules.class); + assertThat(testDescriptor.getChildren().stream().map(TestDescriptor::getDisplayName)).contains( + SimpleRules.class.getSimpleName() + " > " + SimpleRules.SIMPLE_RULE_FIELD_ONE_NAME + ); + + testDescriptor = findRulesDescriptor(archRulesDescriptors, SimpleRuleField.class); + assertThat(testDescriptor.getChildren().stream().map(TestDescriptor::getDisplayName)).contains( + SimpleRuleField.class.getSimpleName() + " > " + SIMPLE_RULE_FIELD_NAME + ); + } + + @Test + void a_class_with_complex_hierarchy() { + EngineDiscoveryTestRequest discoveryRequest = new EngineDiscoveryTestRequest().withClass(ComplexRuleLibrary.class); + + TestDescriptor rootDescriptor = testEngine.discover(discoveryRequest, engineId); + + assertThat(getAllLeafUniqueIds(rootDescriptor)) + .as("all leaf unique ids of complex hierarchy") + .containsOnlyElementsOf(getExpectedIdsForComplexRuleLibrary(engineId)); + } + + @Test + void a_class_with_complex_hierarchy__display_names() { + EngineDiscoveryTestRequest discoveryRequest = new EngineDiscoveryTestRequest().withClass(ComplexRuleLibrary.class); + + TestDescriptor rootDescriptor = testEngine.discover(discoveryRequest, engineId); + + TestDescriptor complexRuleLibraryDescriptor = findRulesDescriptor(rootDescriptor.getChildren(), ComplexRuleLibrary.class); + TestDescriptor simpleRuleLibraryDescriptor = findRulesDescriptor(complexRuleLibraryDescriptor.getChildren(), SimpleRuleLibrary.class); + TestDescriptor simpleRulesDescriptor = findRulesDescriptor(simpleRuleLibraryDescriptor.getChildren(), SimpleRules.class); + assertThat(simpleRulesDescriptor.getChildren().stream().map(TestDescriptor::getDisplayName)).contains( + SimpleRuleLibrary.class.getSimpleName() + + " > " + SimpleRules.class.getSimpleName() + + " > " + SimpleRules.SIMPLE_RULE_FIELD_ONE_NAME + ); + } + + @Test + void private_instance_members() { + EngineDiscoveryTestRequest discoveryRequest = new EngineDiscoveryTestRequest().withClass(ClassWithPrivateTests.class); + + TestDescriptor rootDescriptor = testEngine.discover(discoveryRequest, engineId); + + assertThat(getAllLeafUniqueIds(rootDescriptor)) + .as("all leaf unique ids of private members") + .containsOnly(privateRuleFieldId(engineId), privateRuleMethodId(engineId)); + } + + @Test + void private_instance_libraries() { + EngineDiscoveryTestRequest discoveryRequest = new EngineDiscoveryTestRequest().withClass(LibraryWithPrivateTests.class); + + TestDescriptor rootDescriptor = testEngine.discover(discoveryRequest, engineId); + + assertThat(getAllLeafUniqueIds(rootDescriptor)) + .as("all leaf unique ids of private members") + .containsOnlyElementsOf(privateRuleLibraryIds(engineId)); + } + + @Test + void an_unique_id() { + UniqueId ruleIdToDiscover = simpleRulesId(engineId) + .append(FIELD_SEGMENT_TYPE, SimpleRules.SIMPLE_RULE_FIELD_ONE_NAME); + EngineDiscoveryTestRequest discoveryRequest = new EngineDiscoveryTestRequest().withUniqueId(ruleIdToDiscover); + + TestDescriptor descriptor = getOnlyTest(testEngine.discover(discoveryRequest, engineId)); + + assertThat(descriptor.getUniqueId()).isEqualTo(ruleIdToDiscover); + assertThat(descriptor.getDisplayName()).isEqualTo(SimpleRules.SIMPLE_RULE_FIELD_ONE_NAME); + assertThat(descriptor.getChildren()).isEmpty(); + assertThat(descriptor.getDescendants()).isEmpty(); + assertThat(descriptor.getType()).isEqualTo(TEST); + assertThat(descriptor.getSource().get()).isEqualTo(FieldSource.from(field(SimpleRules.class, SimpleRules.SIMPLE_RULE_FIELD_ONE_NAME))); + assertThat(descriptor.getParent().get().getSource().get()).isEqualTo(ClassSource.from(SimpleRules.class)); + } + + @Test + void multiple_unique_ids() { + UniqueId testId = simpleRulesId(engineId); + UniqueId firstRuleIdToDiscover = testId.append(FIELD_SEGMENT_TYPE, SimpleRules.SIMPLE_RULE_FIELD_ONE_NAME); + UniqueId secondRuleIdToDiscover = testId.append(METHOD_SEGMENT_TYPE, SimpleRules.SIMPLE_RULE_METHOD_ONE_NAME); + EngineDiscoveryTestRequest discoveryRequest = new EngineDiscoveryTestRequest() + .withUniqueId(firstRuleIdToDiscover) + .withUniqueId(secondRuleIdToDiscover); + + TestDescriptor test = getOnlyElement(testEngine.discover(discoveryRequest, engineId).getChildren()); + + Set discoveredRuleIds = toUniqueIds(test); + assertThat(discoveredRuleIds).containsOnly(firstRuleIdToDiscover, secondRuleIdToDiscover); + } + + @Test + void no_redundant_descriptors() { + UniqueId redundantId = simpleRulesId(engineId) + .append(FIELD_SEGMENT_TYPE, SimpleRules.SIMPLE_RULE_FIELD_ONE_NAME); + EngineDiscoveryTestRequest discoveryRequest = new EngineDiscoveryTestRequest() + .withClass(SimpleRules.class) + .withUniqueId(redundantId); + + TestDescriptor rootDescriptor = testEngine.discover(discoveryRequest, engineId); + + TestDescriptor test = getOnlyElement(rootDescriptor.getChildren()); + assertThat(test.getChildren()).as("all children of test").hasSize(4); + List descriptorsWithSpecifiedId = test.getChildren().stream() + .filter(descriptor -> descriptor.getUniqueId().equals(redundantId)) + .collect(toList()); + assertThat(descriptorsWithSpecifiedId) + .as("descriptors with id " + redundantId) + .hasSize(1); + } + + @Test + void no_redundant_library_descriptors() { + UniqueId simpleRulesId = simpleRulesInLibraryId(engineId); + UniqueId ruleIdOne = simpleRulesId.append(FIELD_SEGMENT_TYPE, SimpleRules.SIMPLE_RULE_FIELD_ONE_NAME); + UniqueId ruleIdTwo = simpleRulesId.append(FIELD_SEGMENT_TYPE, SimpleRules.SIMPLE_RULE_FIELD_TWO_NAME); + EngineDiscoveryTestRequest discoveryRequest = new EngineDiscoveryTestRequest() + .withUniqueId(ruleIdOne) + .withUniqueId(ruleIdTwo); + + TestDescriptor rootDescriptor = testEngine.discover(discoveryRequest, engineId); + + TestDescriptor simpleRules = getArchRulesDescriptorsOfOnlyChild(rootDescriptor).collect(onlyElement()); + + assertThat(toUniqueIds(simpleRules)) + .as("ids of requested children of " + SimpleRules.class.getSimpleName()) + .containsOnly(ruleIdOne, ruleIdTwo); + } + + @Test + void classpath_roots() { + EngineDiscoveryTestRequest discoveryRequest = new EngineDiscoveryTestRequest() + .withClasspathRoot(rootOfClass(Test.class)) + .withClasspathRoot(rootOfClass(SimpleRules.class)) + .withClassNameFilter(excludeClassNamePatterns(".*(W|w)rong.*")); + + TestDescriptor rootDescriptor = testEngine.discover(discoveryRequest, engineId); + + assertThat(getAllLeafUniqueIds(rootDescriptor)) + .as("children discovered by " + ClasspathRootSelector.class.getSimpleName()) + .contains(simpleRulesInLibraryId(engineId).append(FIELD_SEGMENT_TYPE, SimpleRules.SIMPLE_RULE_FIELD_ONE_NAME)) + .contains(simpleRuleFieldTestId(engineId)) + .contains(simpleRuleMethodTestId(engineId)); + } + + @Test + void packages() { + EngineDiscoveryTestRequest discoveryRequest = new EngineDiscoveryTestRequest() + .withPackage(SimpleRuleField.class.getPackage().getName()) + .withPackage(SimpleRules.class.getPackage().getName()); + + TestDescriptor rootDescriptor = testEngine.discover(discoveryRequest, engineId); + + assertThat(toUniqueIds(rootDescriptor)) + .as("tests discovered by " + PackageSelector.class.getSimpleName()) + .containsOnly( + engineId.append(CLASS_SEGMENT_TYPE, SimpleRuleField.class.getName()), + engineId.append(CLASS_SEGMENT_TYPE, SimpleRuleMethod.class.getName()), + simpleRulesId(engineId)); + } + + @Test + void subpackages() { + EngineDiscoveryTestRequest discoveryRequest = new EngineDiscoveryTestRequest() + .withPackage(SimpleRuleLibrary.class.getPackage().getName()) + .withClassNameFilter(excludeClassNamePatterns(".*(W|w)rong.*")); + + TestDescriptor rootDescriptor = testEngine.discover(discoveryRequest, engineId); + + assertThat(toUniqueIds(rootDescriptor)) + .as("tests discovered by " + PackageSelector.class.getSimpleName()) + .contains( + engineId.append(CLASS_SEGMENT_TYPE, SimpleRuleLibrary.class.getName()), + simpleRulesId(engineId)); + + assertThat(getAllLeafUniqueIds(rootDescriptor)) + .as("children discovered by " + PackageSelector.class.getSimpleName()) + .contains( + simpleRulesInLibraryId(engineId).append(FIELD_SEGMENT_TYPE, SimpleRules.SIMPLE_RULE_FIELD_ONE_NAME), + simpleRuleFieldTestId(engineId), + simpleRuleMethodTestId(engineId)); + } + + @Test + void methods() { + EngineDiscoveryTestRequest discoveryRequest = new EngineDiscoveryTestRequest() + .withMethod(SimpleRuleMethod.class, SimpleRuleMethod.SIMPLE_RULE_METHOD_NAME) + .withMethod(SimpleRules.class, SimpleRules.SIMPLE_RULE_METHOD_ONE_NAME); + + TestDescriptor rootDescriptor = testEngine.discover(discoveryRequest, engineId); + + assertThat(getAllLeafUniqueIds(rootDescriptor)) + .as("children discovered by " + MethodSelector.class.getSimpleName()) + .containsOnly( + simpleRuleMethodTestId(engineId), + simpleRulesId(engineId) + .append(METHOD_SEGMENT_TYPE, SimpleRules.SIMPLE_RULE_METHOD_ONE_NAME)); + } + + @Test + void fields() { + EngineDiscoveryTestRequest discoveryRequest = new EngineDiscoveryTestRequest() + .withField(SimpleRuleField.class, SimpleRuleField.SIMPLE_RULE_FIELD_NAME) + .withField(SimpleRules.class, SimpleRules.SIMPLE_RULE_FIELD_ONE_NAME); + + TestDescriptor rootDescriptor = testEngine.discover(discoveryRequest, engineId); + + assertThat(getAllLeafUniqueIds(rootDescriptor)) + .as("children discovered by " + FieldSelector.class.getSimpleName()) + .containsOnly( + simpleRuleFieldTestId(engineId), + simpleRulesId(engineId) + .append(FIELD_SEGMENT_TYPE, SimpleRules.SIMPLE_RULE_FIELD_ONE_NAME)); + } + + @Test + void mixed_class_methods_and_fields() { + EngineDiscoveryTestRequest discoveryRequest = new EngineDiscoveryTestRequest() + .withField(SimpleRuleField.class, SimpleRuleField.SIMPLE_RULE_FIELD_NAME) + .withMethod(SimpleRuleMethod.class, SimpleRuleMethod.SIMPLE_RULE_METHOD_NAME) + .withClass(SimpleRules.class); + + TestDescriptor rootDescriptor = testEngine.discover(discoveryRequest, engineId); + + Set expectedLeafIds = new HashSet<>(); + 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))) + .forEach(expectedLeafIds::add); + + assertThat(getAllLeafUniqueIds(rootDescriptor)) + .as("children discovered by mixed selectors") + .containsOnlyElementsOf(expectedLeafIds); + } + + @Test + void tags_of_test_classes() { + EngineDiscoveryTestRequest discoveryRequest = new EngineDiscoveryTestRequest().withClass(TestClassWithTags.class); + + TestDescriptor descriptor = testEngine.discover(discoveryRequest, engineId); + + TestDescriptor testClass = getOnlyElement(descriptor.getChildren()); + assertThat(testClass.getTags()).containsOnly(TestTag.create("tag-one"), TestTag.create("tag-two")); + + Set concreteRules = getAllLeafs(testClass); + assertThat(concreteRules).as("concrete rules").hasSize(3); + concreteRules.forEach(concreteRule -> + assertThat(concreteRule.getTags()).containsOnly(TestTag.create("tag-one"), TestTag.create("tag-two")) + ); + } + + @Test + void meta_tag_of_test_classes() { + EngineDiscoveryTestRequest discoveryRequest = new EngineDiscoveryTestRequest().withClass(TestClassWithMetaTag.class); + + TestDescriptor descriptor = testEngine.discover(discoveryRequest, engineId); + + TestDescriptor testClass = getOnlyElement(descriptor.getChildren()); + assertThat(testClass.getTags()).containsOnly(TestTag.create("meta-tag-one"), TestTag.create("meta-tag-two")); + + Set concreteRules = getAllLeafs(testClass); + assertThat(concreteRules).as("concrete rules").hasSize(3); + concreteRules.forEach(concreteRule -> + assertThat(concreteRule.getTags()).containsOnly(TestTag.create("meta-tag-one"), TestTag.create("meta-tag-two")) + ); + } + + @Test + void meta_tags_of_test_classes() { + EngineDiscoveryTestRequest discoveryRequest = new EngineDiscoveryTestRequest().withClass(TestClassWithMetaTags.class); + + TestDescriptor descriptor = testEngine.discover(discoveryRequest, engineId); + + TestDescriptor testClass = getOnlyElement(descriptor.getChildren()); + assertThat(testClass.getTags()).containsOnly(TestTag.create("meta-tags-one"), TestTag.create("meta-tags-two")); + + Set concreteRules = getAllLeafs(testClass); + assertThat(concreteRules).as("concrete rules").hasSize(3); + concreteRules.forEach(concreteRule -> + assertThat(concreteRule.getTags()).containsOnly(TestTag.create("meta-tags-one"), TestTag.create("meta-tags-two")) + ); + } + + @Test + void tags_of_rule_fields() { + TestDescriptor testField = getOnlyChildWithDescriptorContaining(FIELD_WITH_TAG_NAME, TestFieldWithTags.class); + + assertThat(testField.getTags()).containsOnly(TestTag.create("field-tag-one"), TestTag.create("field-tag-two")); + } + + @Test + void meta_tag_of_rule_fields() { + TestDescriptor testField = getOnlyChildWithDescriptorContaining(FIELD_WITH_META_TAG_NAME, TestFieldWithMetaTag.class); + + assertThat(testField.getTags()).containsOnly(TestTag.create("field-meta-tag-one"), TestTag.create("field-meta-tag-two")); + } + + @Test + void meta_tags_of_rule_fields() { + TestDescriptor testField = getOnlyChildWithDescriptorContaining(FIELD_WITH_META_TAGS_NAME, TestFieldWithMetaTags.class); + + assertThat(testField.getTags()).containsOnly(TestTag.create("field-meta-tags-one"), TestTag.create("field-meta-tags-two")); + } + + @Test + void tags_of_rule_methods() { + TestDescriptor testMethod = getOnlyChildWithDescriptorContaining(METHOD_WITH_TAG_NAME, TestMethodWithTags.class); + + assertThat(testMethod.getTags()).containsOnly(TestTag.create("method-tag-one"), TestTag.create("method-tag-two")); + } + + @Test + void meta_tag_of_rule_methods() { + TestDescriptor testMethod = getOnlyChildWithDescriptorContaining(METHOD_WITH_META_TAG_NAME, TestMethodWithMetaTag.class); + + assertThat(testMethod.getTags()).containsOnly(TestTag.create("method-meta-tag-one"), TestTag.create("method-meta-tag-two")); + } + + @Test + void meta_tags_of_rule_methods() { + TestDescriptor testMethod = getOnlyChildWithDescriptorContaining(METHOD_WITH_META_TAGS_NAME, TestMethodWithMetaTags.class); + + assertThat(testMethod.getTags()).containsOnly(TestTag.create("method-meta-tags-one"), TestTag.create("method-meta-tags-two")); + } + + @Test + void complex_tags() { + EngineDiscoveryTestRequest discoveryRequest = new EngineDiscoveryTestRequest().withClass(ComplexTags.class); + + TestDescriptor descriptor = testEngine.discover(discoveryRequest, engineId); + + Map> tagsById = new HashMap<>(); + descriptor.accept(d -> tagsById.put(d.getUniqueId(), d.getTags())); + + assertThat(getTagsForIdEndingIn(ComplexTags.class.getSimpleName(), tagsById)) + .containsOnly(TestTag.create("library-tag")); + + assertThat(getTagsForIdEndingIn(TestClassWithTags.class.getSimpleName(), tagsById)) + .containsOnly( + TestTag.create("library-tag"), + TestTag.create("rules-tag"), + TestTag.create("tag-one"), + TestTag.create("tag-two")); + + assertThat(getTagsForIdEndingIn(TestClassWithTags.FIELD_RULE_NAME, tagsById)) + .containsOnly( + TestTag.create("library-tag"), + TestTag.create("rules-tag"), + TestTag.create("tag-one"), + TestTag.create("tag-two")); + + assertThat(getTagsForIdEndingIn(ComplexTags.FIELD_RULE_NAME, tagsById)) + .containsOnly( + TestTag.create("library-tag"), + TestTag.create("field-tag")); + + assertThat(getTagsForIdEndingIn(ComplexTags.METHOD_RULE_NAME, tagsById)) + .containsOnly( + TestTag.create("library-tag"), + TestTag.create("method-tag")); + } + + @Test + void complex_meta_tags() { + EngineDiscoveryTestRequest discoveryRequest = new EngineDiscoveryTestRequest().withClass(ComplexMetaTags.class); + + TestDescriptor descriptor = testEngine.discover(discoveryRequest, engineId); + + Map> tagsById = new HashMap<>(); + descriptor.accept(d -> tagsById.put(d.getUniqueId(), d.getTags())); + + assertThat(getTagsForIdEndingIn(ComplexMetaTags.class.getSimpleName(), tagsById)) + .containsOnly(TestTag.create("library-meta-tag")); + + assertThat(getTagsForIdEndingIn(TestClassWithMetaTag.class.getSimpleName(), tagsById)) + .containsOnly( + TestTag.create("library-meta-tag"), + TestTag.create("rules-meta-tag"), + TestTag.create("meta-tag-one"), + TestTag.create("meta-tag-two")); + + assertThat(getTagsForIdEndingIn(TestClassWithMetaTags.class.getSimpleName(), tagsById)) + .containsOnly( + TestTag.create("library-meta-tag"), + TestTag.create("rules-meta-tag"), + TestTag.create("meta-tags-one"), + TestTag.create("meta-tags-two")); + + assertThat(getTagsForIdEndingIn(TestClassWithMetaTag.FIELD_RULE_NAME, tagsById)) + .containsOnly( + TestTag.create("library-meta-tag"), + TestTag.create("rules-meta-tag"), + TestTag.create("meta-tag-one"), + TestTag.create("meta-tag-two")); + + assertThat(getTagsForIdEndingIn(TestClassWithMetaTags.FIELD_RULE_NAME, tagsById)) + .containsOnly( + TestTag.create("library-meta-tag"), + TestTag.create("rules-meta-tag"), + TestTag.create("meta-tags-one"), + TestTag.create("meta-tags-two")); + + assertThat(getTagsForIdEndingIn(ComplexMetaTags.FIELD_RULE_NAME, tagsById)) + .containsOnly( + TestTag.create("library-meta-tag"), + TestTag.create("field-meta-tag")); + + assertThat(getTagsForIdEndingIn(ComplexMetaTags.METHOD_RULE_NAME, tagsById)) + .containsOnly( + TestTag.create("library-meta-tag"), + TestTag.create("method-meta-tag")); + } + + @Test + void filtering_excluded_class_names() { + EngineDiscoveryTestRequest discoveryRequest = new EngineDiscoveryTestRequest() + .withClasspathRoot(uriOfClass(SimpleRuleField.class)) + .withClasspathRoot(uriOfClass(SimpleRuleMethod.class)) + .withClasspathRoot(uriOfClass(SimpleRules.class)) + .withClasspathRoot(uriOfClass(SimpleRuleLibrary.class)) + .withClassNameFilter(excludeClassNamePatterns(".*(Field|Rules).*")); + + TestDescriptor rootDescriptor = testEngine.discover(discoveryRequest, engineId); + + assertThat(toUniqueIds(rootDescriptor)).containsOnly( + engineId.append(CLASS_SEGMENT_TYPE, SimpleRuleMethod.class.getName()), + engineId.append(CLASS_SEGMENT_TYPE, SimpleRuleLibrary.class.getName())); + } + + @Test + void filtering_included_class_names() { + EngineDiscoveryTestRequest discoveryRequest = new EngineDiscoveryTestRequest() + .withClasspathRoot(uriOfClass(SimpleRuleField.class)) + .withClasspathRoot(uriOfClass(SimpleRuleMethod.class)) + .withClasspathRoot(uriOfClass(SimpleRules.class)) + .withClasspathRoot(uriOfClass(SimpleRuleLibrary.class)) + .withClassNameFilter(includeClassNamePatterns(".*(Field|Rules).*")); + + TestDescriptor rootDescriptor = testEngine.discover(discoveryRequest, engineId); + + assertThat(toUniqueIds(rootDescriptor)).containsOnly( + engineId.append(CLASS_SEGMENT_TYPE, SimpleRuleField.class.getName()), + simpleRulesId(engineId)); + } + + @Test + void filtering_excluded_packages() { + EngineDiscoveryTestRequest discoveryRequest = new EngineDiscoveryTestRequest() + .withPackage(SimpleRuleLibrary.class.getPackage().getName()) + .withPackageNameFilter(excludePackageNames( + SimpleRuleField.class.getPackage().getName(), + SimpleRules.class.getPackage().getName(), + WrongRuleMethodNotStatic.class.getPackage().getName())); + + TestDescriptor rootDescriptor = testEngine.discover(discoveryRequest, engineId); + + assertThat(toUniqueIds(rootDescriptor)) + .contains(engineId.append(CLASS_SEGMENT_TYPE, SimpleRuleLibrary.class.getName())) + .doesNotContain( + engineId.append(CLASS_SEGMENT_TYPE, SimpleRuleField.class.getName()), + engineId.append(CLASS_SEGMENT_TYPE, SimpleRuleMethod.class.getName()), + simpleRulesId(engineId)); + } + + @Test + void filtering_included_packages() { + EngineDiscoveryTestRequest discoveryRequest = new EngineDiscoveryTestRequest() + .withPackage(SimpleRuleLibrary.class.getPackage().getName()) + .withPackageNameFilter(includePackageNames( + SimpleRuleField.class.getPackage().getName(), + SimpleRules.class.getPackage().getName())); + + TestDescriptor rootDescriptor = testEngine.discover(discoveryRequest, engineId); + + assertThat(toUniqueIds(rootDescriptor)).containsOnly( + engineId.append(CLASS_SEGMENT_TYPE, SimpleRuleField.class.getName()), + engineId.append(CLASS_SEGMENT_TYPE, SimpleRuleMethod.class.getName()), + simpleRulesId(engineId)); + } + + @Test + void filtering_specific_rule_by_system_property() { + System.setProperty("archunit.junit.testFilter", SimpleRules.SIMPLE_RULE_FIELD_TWO_NAME); + + EngineDiscoveryTestRequest discoveryRequest = new EngineDiscoveryTestRequest() + .withPackage(SimpleRuleLibrary.class.getPackage().getName()) + .withPackageNameFilter(excludePackageNames(WrongRuleMethodNotStatic.class.getPackage().getName())); + + TestDescriptor rootDescriptor = testEngine.discover(discoveryRequest, engineId); + + Set leafIds = getAllLeafUniqueIds(rootDescriptor); + assertThat(leafIds).isNotEmpty(); + leafIds.forEach(leafId -> { + assertThat(leafId.getLastSegment().getType()).isEqualTo(FIELD_SEGMENT_TYPE); + assertThat(leafId.getLastSegment().getValue()).isEqualTo(SimpleRules.SIMPLE_RULE_FIELD_TWO_NAME); + }); + } + + @Test + void filtering_specific_rules_by_system_property() { + System.setProperty("archunit.junit.testFilter", "simple_rule_method_two,simple_rule,some_non_existing_rule"); + + EngineDiscoveryTestRequest discoveryRequest = new EngineDiscoveryTestRequest() + .withClass(SimpleRuleLibrary.class); + + TestDescriptor rootDescriptor = testEngine.discover(discoveryRequest, engineId); + + Set leafIds = getAllLeafUniqueIds(rootDescriptor); + assertThat(leafIds).containsOnly( + simpleRulesInLibraryId(engineId) + .append(METHOD_SEGMENT_TYPE, SimpleRules.SIMPLE_RULE_METHOD_TWO_NAME), + simpleRuleFieldTestId(engineId + .append(CLASS_SEGMENT_TYPE, SimpleRuleLibrary.class.getName()) + .append(FIELD_SEGMENT_TYPE, SimpleRuleLibrary.RULES_TWO_FIELD))); + } + + @Test + void all_without_filters() { + EngineDiscoveryTestRequest discoveryRequest = new EngineDiscoveryTestRequest() + .withClasspathRoot(uriOfClass(SimpleRuleField.class)) + .withClasspathRoot(uriOfClass(SimpleRules.class)); + + TestDescriptor rootDescriptor = testEngine.discover(discoveryRequest, engineId); + + assertThat(toUniqueIds(rootDescriptor)).containsOnly( + engineId.append(CLASS_SEGMENT_TYPE, SimpleRuleField.class.getName()), + simpleRulesId(engineId)); + } + + @Test + void without_importing_classes_already() { + EngineDiscoveryTestRequest discoveryRequest = discoveryRequestUsingAllMethodsOfDiscovery(); + + doThrow(new AssertionError("The cache should not be queried during discovery, " + + "otherwise classes will be imported when tests are possibly skipped afterwards")) + .when(classCache).getClassesToAnalyzeFor(any(), any()); + + TestDescriptor rootDescriptor = testEngine.discover(discoveryRequest, engineId); + + assertThat(rootDescriptor).isNotNull(); + } + + @Test + @ExtendWith(TestLogExtension.class) + void without_scanning_unnecessary_classes_for_tests(LogCaptor logCaptor) { + EngineDiscoveryTestRequest discoveryRequest = discoveryRequestUsingAllMethodsOfDiscovery(); + + logCaptor.watch(ClassFileImporter.class, DEBUG); + testEngine.discover(discoveryRequest, engineId); + + // since we discover the classes from the classpath with ArchUnit we expect DEBUG log entries for the test classes + assertThat(logCaptor.getEvents(DEBUG).stream()) + .extracting(RecordedLogEvent::getMessage) + .as("messages of log events with level " + DEBUG) + .anySatisfy(message -> assertThat(message) + .matches(".*Processing class '.*" + SimpleRuleField.class.getSimpleName() + "'")); + + // but we do not want to transitively resolve dependencies of those classes like java.lang.Object + assertThat(logCaptor.getEvents(DEBUG).stream()) + .extracting(RecordedLogEvent::getMessage) + .as("messages of log events with level " + DEBUG) + .noneSatisfy(message -> assertThat(message) + .matches(".*Processing class '.*" + Object.class.getSimpleName() + "'")); + } + + private EngineDiscoveryTestRequest discoveryRequestUsingAllMethodsOfDiscovery() { + return new EngineDiscoveryTestRequest() + .withClass(SimpleRuleField.class) + .withClasspathRoot(uriOfClass(SimpleRuleField.class)) + .withField(SimpleRuleField.class, SimpleRuleField.SIMPLE_RULE_FIELD_NAME) + .withPackage(SimpleRuleField.class.getPackage().getName()) + .withMethod(SimpleRuleMethod.class, SimpleRuleMethod.SIMPLE_RULE_METHOD_NAME) + .withUniqueId(simpleRulesId(engineId).append(FIELD_SEGMENT_TYPE, SimpleRules.SIMPLE_RULE_FIELD_ONE_NAME)); + } + + private Set getTagsForIdEndingIn(String suffix, Map> tagsById) { + UniqueId matchingId = tagsById.keySet().stream() + .filter(id -> getLast(id.getSegments()).getValue().endsWith(suffix)) + .collect(onlyElement()); + return tagsById.get(matchingId); + } + + private TestDescriptor getOnlyChildWithDescriptorContaining(String idPart, Class testClass) { + EngineDiscoveryTestRequest discoveryRequest = new EngineDiscoveryTestRequest().withClass(testClass); + + TestDescriptor descriptor = testEngine.discover(discoveryRequest, engineId); + + return getArchRulesDescriptorsOfOnlyChild(descriptor) + .filter(d -> d.getUniqueId().toString().contains(idPart)) + .collect(onlyElement()); + } + + private Stream getArchRulesDescriptorsOfOnlyChild(TestDescriptor descriptor) { + TestDescriptor testClass = getOnlyElement(descriptor.getChildren()); + Set archRulesDescriptors = testClass.getChildren(); + return archRulesDescriptors.stream().map(identity()); + } + + private void assertClassSource(TestDescriptor child, Class aClass) { + ClassSource classSource = (ClassSource) child.getSource().get(); + assertThat(classSource.getClassName()).isEqualTo(aClass.getName()); + assertThat(classSource.getJavaClass()).isEqualTo(aClass); + assertThat(classSource.getPosition().isPresent()).as("position is present").isFalse(); + } + + private void assertNoIntermediaryTestSource(TestDescriptor testDescriptor) { + // Test executors like Gradle test support traverse up the descriptor hierarchy until they find + // a test descriptor with a class source that they then pick for the test report. + // We want to show the original class used to trigger the tests for this to avoid confusion. + assertThat(testDescriptor.getSource()).as("source of intermediary test descriptor").isEmpty(); + } + + private TestDescriptor findRulesDescriptor(Collection archRulesDescriptors, Class clazz) { + return archRulesDescriptors.stream().filter(d -> d.getUniqueId().toString().contains(clazz.getSimpleName())).findFirst().get(); + } + } + + @Nested + class Executes { + @Test + void a_simple_rule_field_without_violation() { + simulateCachedClassesForTest(SimpleRuleField.class, UnwantedClass.CLASS_SATISFYING_RULES); + + EngineExecutionTestListener testListener = execute(engineId, SimpleRuleField.class); + + testListener.verifySuccessful(simpleRuleFieldTestId(engineId)); + } + + @Test + void a_simple_rule_field_with_violation() { + simulateCachedClassesForTest(SimpleRuleField.class, UnwantedClass.CLASS_VIOLATING_RULES); + + EngineExecutionTestListener testListener = execute(engineId, SimpleRuleField.class); + + testListener.verifyViolation(simpleRuleFieldTestId(engineId), UnwantedClass.CLASS_VIOLATING_RULES.getSimpleName()); + } + + @Test + void instance_field_rule_in_abstract_base_class() { + simulateCachedClassesForTest(ArchTestWithAbstractBaseClassWithFieldRule.class, UnwantedClass.CLASS_SATISFYING_RULES); + + EngineExecutionTestListener testListener = execute(engineId, ArchTestWithAbstractBaseClassWithFieldRule.class); + + testListener.verifySuccessful(engineId + .append(CLASS_SEGMENT_TYPE, ArchTestWithAbstractBaseClassWithFieldRule.class.getName()) + .append(FIELD_SEGMENT_TYPE, ArchTestWithAbstractBaseClassWithFieldRule.INSTANCE_FIELD_NAME)); + } + + @Test + void a_simple_rule_method_without_violation() { + simulateCachedClassesForTest(SimpleRuleMethod.class, UnwantedClass.CLASS_SATISFYING_RULES); + + EngineExecutionTestListener testListener = execute(engineId, SimpleRuleMethod.class); + + testListener.verifySuccessful(simpleRuleMethodTestId(engineId)); + } + + @Test + void a_simple_rule_method_with_violation() { + simulateCachedClassesForTest(SimpleRuleMethod.class, UnwantedClass.CLASS_VIOLATING_RULES); + + EngineExecutionTestListener testListener = execute(engineId, SimpleRuleMethod.class); + + testListener.verifyViolation(simpleRuleMethodTestId(engineId), UnwantedClass.CLASS_VIOLATING_RULES.getSimpleName()); + } + + @Test + void instance_method_rule_in_abstract_base_class() { + simulateCachedClassesForTest(ArchTestWithAbstractBaseClassWithMethodRule.class, UnwantedClass.CLASS_SATISFYING_RULES); + + EngineExecutionTestListener testListener = execute(engineId, ArchTestWithAbstractBaseClassWithMethodRule.class); + + testListener.verifySuccessful(engineId + .append(CLASS_SEGMENT_TYPE, ArchTestWithAbstractBaseClassWithMethodRule.class.getName()) + .append(METHOD_SEGMENT_TYPE, ArchTestWithAbstractBaseClassWithMethodRule.INSTANCE_METHOD_NAME)); + } + + @Test + void rule_library_without_violation() { + simulateCachedClassesForTest(SimpleRuleLibrary.class, UnwantedClass.CLASS_SATISFYING_RULES); + + EngineExecutionTestListener testListener = execute(engineId, SimpleRuleLibrary.class); + + getExpectedIdsForSimpleRuleLibrary(engineId).forEach(testListener::verifySuccessful); + } + + @Test + void rule_library_with_violation() { + simulateCachedClassesForTest(SimpleRuleLibrary.class, UnwantedClass.CLASS_VIOLATING_RULES); + + EngineExecutionTestListener testListener = execute(engineId, SimpleRuleLibrary.class); + + getExpectedIdsForSimpleRuleLibrary(engineId).forEach(testId -> + testListener.verifyViolation(testId, UnwantedClass.CLASS_VIOLATING_RULES.getSimpleName())); + } + + @Test + void private_instance_libraries() { + simulateCachedClassesForTest(LibraryWithPrivateTests.class, UnwantedClass.CLASS_VIOLATING_RULES); + + EngineExecutionTestListener testListener = execute(engineId, LibraryWithPrivateTests.class); + + privateRuleLibraryIds(engineId).forEach(testId -> + testListener.verifyViolation(testId, UnwantedClass.CLASS_VIOLATING_RULES.getSimpleName())); + } + + @Test + public void library_with_rules_in_abstract_base_class() { + simulateCachedClassesForTest(ArchTestWithLibraryWithAbstractBaseClass.class, UnwantedClass.CLASS_SATISFYING_RULES); + + EngineExecutionTestListener testListener = execute(engineId, ArchTestWithLibraryWithAbstractBaseClass.class); + + testListener.verifySuccessful(engineId + .append(CLASS_SEGMENT_TYPE, ArchTestWithLibraryWithAbstractBaseClass.class.getName()) + .append(FIELD_SEGMENT_TYPE, ArchTestWithLibraryWithAbstractBaseClass.FIELD_RULE_LIBRARY_NAME) + .append(CLASS_SEGMENT_TYPE, ArchTestWithAbstractBaseClassWithFieldRule.class.getName()) + .append(FIELD_SEGMENT_TYPE, ArchTestWithAbstractBaseClassWithFieldRule.INSTANCE_FIELD_NAME) + ); + testListener.verifySuccessful(engineId + .append(CLASS_SEGMENT_TYPE, ArchTestWithLibraryWithAbstractBaseClass.class.getName()) + .append(FIELD_SEGMENT_TYPE, ArchTestWithLibraryWithAbstractBaseClass.METHOD_RULE_LIBRARY_NAME) + .append(CLASS_SEGMENT_TYPE, ArchTestWithAbstractBaseClassWithMethodRule.class.getName()) + .append(METHOD_SEGMENT_TYPE, ArchTestWithAbstractBaseClassWithMethodRule.INSTANCE_METHOD_NAME) + ); + } + + @Test + void rule_by_unique_id_without_violation() { + UniqueId fieldRuleInLibrary = simpleRulesInLibraryId(engineId) + .append(FIELD_SEGMENT_TYPE, SimpleRules.SIMPLE_RULE_FIELD_ONE_NAME); + simulateCachedClassesForTest(SimpleRuleLibrary.class, UnwantedClass.CLASS_SATISFYING_RULES); + + EngineExecutionTestListener testListener = execute(engineId, new EngineDiscoveryTestRequest() + .withUniqueId(fieldRuleInLibrary)); + + testListener.verifySuccessful(fieldRuleInLibrary); + testListener.verifyNoOtherStartExceptHierarchyOf(fieldRuleInLibrary); + } + + @Test + void rule_by_unique_id_with_violation() { + UniqueId fieldRuleInLibrary = simpleRulesInLibraryId(engineId) + .append(FIELD_SEGMENT_TYPE, SimpleRules.SIMPLE_RULE_FIELD_ONE_NAME); + simulateCachedClassesForTest(SimpleRuleLibrary.class, UnwantedClass.CLASS_VIOLATING_RULES); + + EngineExecutionTestListener testListener = execute(engineId, new EngineDiscoveryTestRequest() + .withUniqueId(fieldRuleInLibrary)); + + testListener.verifyViolation(fieldRuleInLibrary, UnwantedClass.CLASS_VIOLATING_RULES.getSimpleName()); + testListener.verifyNoOtherStartExceptHierarchyOf(fieldRuleInLibrary); + } + + @Test + void mixed_rules_by_unique_id_and_class_with_violation() { + UniqueId fieldRuleInLibrary = simpleRulesInLibraryId(engineId) + .append(FIELD_SEGMENT_TYPE, SimpleRules.SIMPLE_RULE_FIELD_ONE_NAME); + UniqueId methodRuleInLibrary = simpleRulesInLibraryId(engineId) + .append(METHOD_SEGMENT_TYPE, SimpleRules.SIMPLE_RULE_METHOD_ONE_NAME); + simulateCachedClassesForTest(SimpleRuleLibrary.class, UnwantedClass.CLASS_VIOLATING_RULES); + simulateCachedClassesForTest(SimpleRuleField.class, UnwantedClass.CLASS_VIOLATING_RULES); + + EngineExecutionTestListener testListener = execute(engineId, new EngineDiscoveryTestRequest() + .withClass(SimpleRuleField.class) + .withUniqueId(fieldRuleInLibrary) + .withUniqueId(methodRuleInLibrary)); + + testListener.verifyViolation(simpleRuleFieldTestId(engineId), UnwantedClass.CLASS_VIOLATING_RULES.getSimpleName()); + testListener.verifyViolation(fieldRuleInLibrary, UnwantedClass.CLASS_VIOLATING_RULES.getSimpleName()); + testListener.verifyViolation(methodRuleInLibrary, UnwantedClass.CLASS_VIOLATING_RULES.getSimpleName()); + } + + @Test + void passes_AnalyzeClasses_to_cache() { + execute(createEngineId(), FullAnalyzeClassesSpec.class); + + verify(classCache).getClassesToAnalyzeFor(eq(FullAnalyzeClassesSpec.class), classAnalysisRequestCaptor.capture()); + ClassAnalysisRequest request = classAnalysisRequestCaptor.getValue(); + AnalyzeClasses expected = FullAnalyzeClassesSpec.class.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()); + } + + @Test + void cache_is_cleared_afterwards() { + execute(createEngineId(), SimpleRuleLibrary.class); + + verify(classCache, times(1)).clear(SimpleRuleLibrary.class); + verify(classCache, atLeastOnce()).getClassesToAnalyzeFor(any(Class.class), any(ClassAnalysisRequest.class)); + verifyNoMoreInteractions(classCache); + } + + @Test + void a_class_with_analyze_classes_as_meta_annotation() { + execute(createEngineId(), TestClassWithMetaAnnotationForAnalyzeClasses.class); + + verify(classCache).getClassesToAnalyzeFor(eq(TestClassWithMetaAnnotationForAnalyzeClasses.class), classAnalysisRequestCaptor.capture()); + ClassAnalysisRequest request = classAnalysisRequestCaptor.getValue(); + AnalyzeClasses expected = TestClassWithMetaAnnotationForAnalyzeClasses.class.getAnnotation(TestClassWithMetaAnnotationForAnalyzeClasses.MetaAnalyzeClasses.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 + class Rejects { + @Test + void rule_method_with_wrong_parameters() { + EngineDiscoveryTestRequest discoveryRequest = new EngineDiscoveryTestRequest().withClass(WrongRuleMethodWrongParameters.class); + + assertThatThrownBy(() -> testEngine.discover(discoveryRequest, engineId)) + .isInstanceOf(ArchTestInitializationException.class) + .hasMessageContaining(ArchTest.class.getSimpleName()) + .hasMessageContaining(WrongRuleMethodWrongParameters.class.getSimpleName()) + .hasMessageContaining(WrongRuleMethodWrongParameters.WRONG_PARAMETERS_METHOD_NAME) + .hasMessageContaining("must have exactly one parameter of type " + JavaClasses.class.getName()); + } + } + + @Nested + class Ignores { + @Test + void fields() { + simulateCachedClassesForTest(IgnoredField.class, UnwantedClass.CLASS_VIOLATING_RULES); + + EngineExecutionTestListener testListener = execute(engineId, IgnoredField.class); + + testListener.verifySkipped(engineId + .append(CLASS_SEGMENT_TYPE, IgnoredField.class.getName()) + .append(FIELD_SEGMENT_TYPE, IgnoredField.IGNORED_RULE_FIELD)); + + testListener.verifyViolation(engineId + .append(CLASS_SEGMENT_TYPE, IgnoredField.class.getName()) + .append(FIELD_SEGMENT_TYPE, IgnoredField.UNIGNORED_RULE_FIELD)); + } + + @Test + void methods() { + simulateCachedClassesForTest(IgnoredMethod.class, UnwantedClass.CLASS_VIOLATING_RULES); + + EngineExecutionTestListener testListener = execute(engineId, IgnoredMethod.class); + + testListener.verifySkipped(engineId + .append(CLASS_SEGMENT_TYPE, IgnoredMethod.class.getName()) + .append(METHOD_SEGMENT_TYPE, IgnoredMethod.IGNORED_RULE_METHOD)); + + testListener.verifyViolation(engineId + .append(CLASS_SEGMENT_TYPE, IgnoredMethod.class.getName()) + .append(METHOD_SEGMENT_TYPE, IgnoredMethod.UNIGNORED_RULE_METHOD)); + } + + @Test + void classes() { + simulateCachedClassesForTest(IgnoredClass.class, UnwantedClass.CLASS_VIOLATING_RULES); + + EngineExecutionTestListener testListener = execute(engineId, IgnoredClass.class); + + testListener.verifySkipped(engineId + .append(CLASS_SEGMENT_TYPE, IgnoredClass.class.getName())); + + testListener.verifyNoOtherStartExceptHierarchyOf(engineId); + } + + @Test + void libraries() { + simulateCachedClassesForTest(IgnoredLibrary.class, UnwantedClass.CLASS_VIOLATING_RULES); + + EngineExecutionTestListener testListener = execute(engineId, IgnoredLibrary.class); + + testListener.verifySkipped(engineId + .append(CLASS_SEGMENT_TYPE, IgnoredLibrary.class.getName()) + .append(FIELD_SEGMENT_TYPE, IgnoredLibrary.IGNORED_LIB_FIELD) + .append(CLASS_SEGMENT_TYPE, SimpleRules.class.getName())); + } + + @Test + void library_referenced_classes() { + simulateCachedClassesForTest(IgnoredLibrary.class, UnwantedClass.CLASS_VIOLATING_RULES); + + EngineExecutionTestListener testListener = execute(engineId, IgnoredLibrary.class); + + testListener.verifySkipped(engineId + .append(CLASS_SEGMENT_TYPE, IgnoredLibrary.class.getName()) + .append(FIELD_SEGMENT_TYPE, IgnoredLibrary.UNIGNORED_LIB_ONE_FIELD) + .append(CLASS_SEGMENT_TYPE, IgnoredClass.class.getName())); + } + + @Test + void library_sub_rules() { + simulateCachedClassesForTest(IgnoredLibrary.class, UnwantedClass.CLASS_VIOLATING_RULES); + + EngineExecutionTestListener testListener = execute(engineId, IgnoredLibrary.class); + + UniqueId classWithIgnoredMethod = engineId + .append(CLASS_SEGMENT_TYPE, IgnoredLibrary.class.getName()) + .append(FIELD_SEGMENT_TYPE, IgnoredLibrary.UNIGNORED_LIB_TWO_FIELD) + .append(CLASS_SEGMENT_TYPE, IgnoredMethod.class.getName()); + + testListener.verifySkipped(classWithIgnoredMethod + .append(METHOD_SEGMENT_TYPE, IgnoredMethod.IGNORED_RULE_METHOD)); + + testListener.verifyViolation(classWithIgnoredMethod + .append(METHOD_SEGMENT_TYPE, IgnoredMethod.UNIGNORED_RULE_METHOD)); + } + + @Test + void with_reason() { + simulateCachedClassesForTest(IgnoredField.class, UnwantedClass.CLASS_VIOLATING_RULES); + + EngineExecutionTestListener testListener = execute(engineId, IgnoredField.class); + + UniqueId ignoredId = engineId + .append(CLASS_SEGMENT_TYPE, IgnoredField.class.getName()) + .append(FIELD_SEGMENT_TYPE, IgnoredField.IGNORED_RULE_FIELD); + testListener.verifySkipped(ignoredId, "some example description"); + } + } + + @Nested + class MetaIgnores { + @Test + void fields() { + simulateCachedClassesForTest(MetaIgnoredField.class, UnwantedClass.CLASS_VIOLATING_RULES); + + EngineExecutionTestListener testListener = execute(engineId, MetaIgnoredField.class); + + testListener.verifySkipped(engineId + .append(CLASS_SEGMENT_TYPE, MetaIgnoredField.class.getName()) + .append(FIELD_SEGMENT_TYPE, MetaIgnoredField.IGNORED_RULE_FIELD)); + + testListener.verifyViolation(engineId + .append(CLASS_SEGMENT_TYPE, MetaIgnoredField.class.getName()) + .append(FIELD_SEGMENT_TYPE, MetaIgnoredField.UNIGNORED_RULE_FIELD)); + } + + @Test + void methods() { + simulateCachedClassesForTest(MetaIgnoredMethod.class, UnwantedClass.CLASS_VIOLATING_RULES); + + EngineExecutionTestListener testListener = execute(engineId, MetaIgnoredMethod.class); + + testListener.verifySkipped(engineId + .append(CLASS_SEGMENT_TYPE, MetaIgnoredMethod.class.getName()) + .append(METHOD_SEGMENT_TYPE, MetaIgnoredMethod.IGNORED_RULE_METHOD)); + + testListener.verifyViolation(engineId + .append(CLASS_SEGMENT_TYPE, MetaIgnoredMethod.class.getName()) + .append(METHOD_SEGMENT_TYPE, MetaIgnoredMethod.UNIGNORED_RULE_METHOD)); + } + + @Test + void classes() { + simulateCachedClassesForTest(MetaIgnoredClass.class, UnwantedClass.CLASS_VIOLATING_RULES); + + EngineExecutionTestListener testListener = execute(engineId, MetaIgnoredClass.class); + + testListener.verifySkipped(engineId + .append(CLASS_SEGMENT_TYPE, MetaIgnoredClass.class.getName())); + + testListener.verifyNoOtherStartExceptHierarchyOf(engineId); + } + + @Test + void libraries() { + simulateCachedClassesForTest(MetaIgnoredLibrary.class, UnwantedClass.CLASS_VIOLATING_RULES); + + EngineExecutionTestListener testListener = execute(engineId, MetaIgnoredLibrary.class); + + testListener.verifySkipped(engineId + .append(CLASS_SEGMENT_TYPE, MetaIgnoredLibrary.class.getName()) + .append(FIELD_SEGMENT_TYPE, MetaIgnoredLibrary.IGNORED_LIB_FIELD) + .append(CLASS_SEGMENT_TYPE, SimpleRules.class.getName())); + } + + @Test + void library_referenced_classes() { + simulateCachedClassesForTest(MetaIgnoredLibrary.class, UnwantedClass.CLASS_VIOLATING_RULES); + + EngineExecutionTestListener testListener = execute(engineId, MetaIgnoredLibrary.class); + + testListener.verifySkipped(engineId + .append(CLASS_SEGMENT_TYPE, MetaIgnoredLibrary.class.getName()) + .append(FIELD_SEGMENT_TYPE, MetaIgnoredLibrary.UNIGNORED_LIB_ONE_FIELD) + .append(CLASS_SEGMENT_TYPE, MetaIgnoredClass.class.getName())); + } + + @Test + void library_sub_rules() { + simulateCachedClassesForTest(MetaIgnoredLibrary.class, UnwantedClass.CLASS_VIOLATING_RULES); + + EngineExecutionTestListener testListener = execute(engineId, MetaIgnoredLibrary.class); + + UniqueId classWithIgnoredMethod = engineId + .append(CLASS_SEGMENT_TYPE, MetaIgnoredLibrary.class.getName()) + .append(FIELD_SEGMENT_TYPE, MetaIgnoredLibrary.UNIGNORED_LIB_TWO_FIELD) + .append(CLASS_SEGMENT_TYPE, MetaIgnoredMethod.class.getName()); + + testListener.verifySkipped(classWithIgnoredMethod + .append(METHOD_SEGMENT_TYPE, MetaIgnoredMethod.IGNORED_RULE_METHOD)); + + testListener.verifyViolation(classWithIgnoredMethod + .append(METHOD_SEGMENT_TYPE, MetaIgnoredMethod.UNIGNORED_RULE_METHOD)); + } + + @Test + void with_reason() { + simulateCachedClassesForTest(MetaIgnoredField.class, UnwantedClass.CLASS_VIOLATING_RULES); + + EngineExecutionTestListener testListener = execute(engineId, MetaIgnoredField.class); + + UniqueId ignoredId = engineId + .append(CLASS_SEGMENT_TYPE, MetaIgnoredField.class.getName()) + .append(FIELD_SEGMENT_TYPE, MetaIgnoredField.IGNORED_RULE_FIELD); + testListener.verifySkipped(ignoredId, "some example description"); + } + } + + @Nested + class GeneratesDisplayName { + @Test + void for_field_by_replacing_underscores_with_blanks_if_property_is_set_to_true() { + EngineDiscoveryTestRequest discoveryRequest = new EngineDiscoveryTestRequest().withClass(SimpleRuleField.class); + + ArchConfiguration.get().setProperty(DisplayNameResolver.JUNIT_DISPLAYNAME_REPLACE_UNDERSCORES_BY_SPACES_PROPERTY_NAME, "true"); + + TestDescriptor descriptor = testEngine.discover(discoveryRequest, engineId); + + TestDescriptor ruleDescriptor = getOnlyTest(descriptor); + + assertThat(ruleDescriptor.getDisplayName()).isEqualTo("simple rule"); + } + + @Test + void for_method_by_replacing_underscores_with_blanks_if_property_is_set_to_true() { + EngineDiscoveryTestRequest discoveryRequest = new EngineDiscoveryTestRequest().withClass(SimpleRuleMethod.class); + + ArchConfiguration.get().setProperty(DisplayNameResolver.JUNIT_DISPLAYNAME_REPLACE_UNDERSCORES_BY_SPACES_PROPERTY_NAME, "true"); + + TestDescriptor descriptor = testEngine.discover(discoveryRequest, engineId); + + TestDescriptor ruleDescriptor = getOnlyTest(descriptor); + + assertThat(ruleDescriptor.getDisplayName()).isEqualTo("simple rule"); + } + + @Test + void by_returning_original_name_if_property_is_set_to_false() { + EngineDiscoveryTestRequest discoveryRequest = new EngineDiscoveryTestRequest().withClass(SimpleRuleField.class); + + ArchConfiguration.get().setProperty(DisplayNameResolver.JUNIT_DISPLAYNAME_REPLACE_UNDERSCORES_BY_SPACES_PROPERTY_NAME, "false"); + + TestDescriptor descriptor = testEngine.discover(discoveryRequest, engineId); + + TestDescriptor ruleDescriptor = getOnlyTest(descriptor); + + assertThat(ruleDescriptor.getDisplayName()).isEqualTo("simple_rule"); + } + + @AfterEach + void resetConfiguration() { + ArchConfiguration.get().reset(); + } + } + + private UniqueId createEngineId() { + return UniqueId.forEngine(ArchUnitTestEngine.UNIQUE_ID); + } + + private UniqueId simpleRuleFieldTestId(UniqueId uniqueId) { + return uniqueId + .append(CLASS_SEGMENT_TYPE, SimpleRuleField.class.getName()) + .append(FIELD_SEGMENT_TYPE, SIMPLE_RULE_FIELD_NAME); + } + + private UniqueId simpleRuleMethodTestId(UniqueId uniqueId) { + return uniqueId + .append(CLASS_SEGMENT_TYPE, SimpleRuleMethod.class.getName()) + .append(METHOD_SEGMENT_TYPE, SIMPLE_RULE_METHOD_NAME); + } + + private UniqueId simpleRulesId(UniqueId rootId) { + return rootId.append(CLASS_SEGMENT_TYPE, SimpleRules.class.getName()); + } + + private UniqueId simpleRulesInLibraryId(UniqueId uniqueId) { + return simpleRulesId(uniqueId + .append(CLASS_SEGMENT_TYPE, SimpleRuleLibrary.class.getName()) + .append(FIELD_SEGMENT_TYPE, SimpleRuleLibrary.RULES_ONE_FIELD)); + } + + private Set getExpectedIdsForSimpleRuleLibrary(UniqueId uniqueId) { + UniqueId simpleRuleLibrary = uniqueId.append(CLASS_SEGMENT_TYPE, SimpleRuleLibrary.class.getName()); + Set simpleRulesIds = getExpectedIdsForSimpleRules( + simpleRuleLibrary.append(FIELD_SEGMENT_TYPE, SimpleRuleLibrary.RULES_ONE_FIELD)); + Set simpleRuleFieldIds = singleton(simpleRuleFieldTestId( + simpleRuleLibrary.append(FIELD_SEGMENT_TYPE, SimpleRuleLibrary.RULES_TWO_FIELD))); + + return Stream.of(simpleRulesIds, simpleRuleFieldIds) + .flatMap(Set::stream).collect(toSet()); + } + + private Set getExpectedIdsForSimpleRules(UniqueId append) { + UniqueId simpleRules = simpleRulesId(append); + Set simpleRulesFields = SimpleRules.RULE_FIELD_NAMES.stream().map(fieldName -> simpleRules + .append(FIELD_SEGMENT_TYPE, fieldName)).collect(toSet()); + Set simpleRulesMethods = SimpleRules.RULE_METHOD_NAMES.stream().map(methodName -> simpleRules + .append(METHOD_SEGMENT_TYPE, methodName)).collect(toSet()); + return Stream.of(simpleRulesFields, simpleRulesMethods).flatMap(Set::stream).collect(toSet()); + } + + private Set getExpectedIdsForComplexRuleLibrary(UniqueId uniqueId) { + UniqueId complexRuleLibrary = uniqueId.append(CLASS_SEGMENT_TYPE, ComplexRuleLibrary.class.getName()); + Set simpleRuleLibraryIds = getExpectedIdsForSimpleRuleLibrary(complexRuleLibrary + .append(FIELD_SEGMENT_TYPE, ComplexRuleLibrary.RULES_ONE_FIELD)); + Set simpleRulesIds = getExpectedIdsForSimpleRules(complexRuleLibrary + .append(FIELD_SEGMENT_TYPE, ComplexRuleLibrary.RULES_TWO_FIELD)); + + return Stream.of(simpleRuleLibraryIds, simpleRulesIds).flatMap(Set::stream).collect(toSet()); + } + + private Set privateRuleLibraryIds(UniqueId uniqueId) { + UniqueId libraryId = uniqueId + .append(CLASS_SEGMENT_TYPE, LibraryWithPrivateTests.class.getName()) + .append(FIELD_SEGMENT_TYPE, LibraryWithPrivateTests.PRIVATE_RULES_FIELD_NAME) + .append(CLASS_SEGMENT_TYPE, LibraryWithPrivateTests.SubRules.class.getName()) + .append(FIELD_SEGMENT_TYPE, LibraryWithPrivateTests.SubRules.PRIVATE_RULES_FIELD_NAME); + return ImmutableSet.of(privateRuleFieldId(libraryId), privateRuleMethodId(libraryId)); + } + + private UniqueId privateRuleFieldId(UniqueId uniqueId) { + return uniqueId + .append(CLASS_SEGMENT_TYPE, ClassWithPrivateTests.class.getName()) + .append(FIELD_SEGMENT_TYPE, ClassWithPrivateTests.PRIVATE_RULE_FIELD_NAME); + } + + private UniqueId privateRuleMethodId(UniqueId uniqueId) { + return uniqueId + .append(CLASS_SEGMENT_TYPE, ClassWithPrivateTests.class.getName()) + .append(METHOD_SEGMENT_TYPE, ClassWithPrivateTests.PRIVATE_RULE_METHOD_NAME); + } + + private Set getAllLeafUniqueIds(TestDescriptor rootDescriptor) { + return getAllLeafs(rootDescriptor).stream().map(TestDescriptor::getUniqueId).collect(toSet()); + } + + private TestDescriptor getOnlyTest(TestDescriptor descriptor) { + TestDescriptor testClass = getOnlyElement(descriptor.getChildren()); + TestDescriptor ruleDescriptor = getOnlyElement(testClass.getChildren()); + assertThat(ruleDescriptor.getType()).isEqualTo(TEST); + return ruleDescriptor; + } + + private Set getAllLeafs(TestDescriptor descriptor) { + Set result = new HashSet<>(); + descriptor.accept(possibleLeaf -> { + if (possibleLeaf.getChildren().isEmpty()) { + result.add(possibleLeaf); + } + }); + return result; + } + + private Set toUniqueIds(TestDescriptor rootDescriptor) { + return rootDescriptor.getChildren().stream().map(TestDescriptor::getUniqueId).collect(toSet()); + } + + private void simulateCachedClassesForTest(Class testClass, Class classToReturn) { + when(classCache.getClassesToAnalyzeFor(eq(testClass), classAnalysisRequestOf(testClass))) + .thenReturn(importClasses(classToReturn)); + } + + private ClassAnalysisRequest classAnalysisRequestOf(Class testClass) { + return argThat(r -> Arrays.equals(r.getPackageNames(), testClass.getAnnotation(AnalyzeClasses.class).packages())); + } + + private EngineExecutionTestListener execute(UniqueId uniqueId, Class testClass) { + return execute(uniqueId, new EngineDiscoveryTestRequest().withClass(testClass)); + } + + private EngineExecutionTestListener execute(UniqueId uniqueId, EngineDiscoveryTestRequest discoveryRequest) { + TestDescriptor descriptor = testEngine.discover(discoveryRequest, uniqueId); + + EngineExecutionTestListener listener = new EngineExecutionTestListener(); + testEngine.execute(new ExecutionRequest(descriptor, listener, discoveryRequest.getConfigurationParameters())); + return listener; + } + + private static URI rootOfClass(Class clazz) { + String resourceName = classFileResource(clazz); + URL classResource = clazz.getResource(resourceName); + String rootPath = classResource.toExternalForm() + .replace(resourceName, "") + .replaceAll("^jar:", "") + .replaceAll("!$", ""); + return URI.create(rootPath); + } + + private static URI uriOfClass(Class clazz) { + String resourceName = classFileResource(clazz); + return URI.create(clazz.getResource(resourceName).toExternalForm()); + } + + private static String classFileResource(Class clazz) { + return String.format("/%s.class", clazz.getName().replace('.', '/')); + } +} diff --git a/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/EngineDiscoveryTestRequest.java b/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/EngineDiscoveryTestRequest.java new file mode 100644 index 0000000000..f2a4d49cd4 --- /dev/null +++ b/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/EngineDiscoveryTestRequest.java @@ -0,0 +1,175 @@ +package com.tngtech.archunit.junit.internal; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.net.URI; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +import com.tngtech.archunit.core.domain.JavaClasses; +import com.tngtech.archunit.junit.engine_api.FieldSelector; +import org.junit.platform.engine.ConfigurationParameters; +import org.junit.platform.engine.DiscoveryFilter; +import org.junit.platform.engine.DiscoverySelector; +import org.junit.platform.engine.EngineDiscoveryRequest; +import org.junit.platform.engine.UniqueId; +import org.junit.platform.engine.discovery.ClassNameFilter; +import org.junit.platform.engine.discovery.ClassSelector; +import org.junit.platform.engine.discovery.ClasspathRootSelector; +import org.junit.platform.engine.discovery.DiscoverySelectors; +import org.junit.platform.engine.discovery.MethodSelector; +import org.junit.platform.engine.discovery.PackageNameFilter; +import org.junit.platform.engine.discovery.PackageSelector; +import org.junit.platform.engine.discovery.UniqueIdSelector; + +import static com.tngtech.archunit.junit.engine_api.FieldSelector.selectField; +import static java.util.Collections.emptyList; +import static java.util.Collections.emptySet; +import static java.util.stream.Collectors.toList; +import static java.util.stream.Collectors.toSet; +import static org.junit.platform.engine.discovery.DiscoverySelectors.selectMethod; + +class EngineDiscoveryTestRequest implements EngineDiscoveryRequest { + private final List classpathRootsToDiscover = new ArrayList<>(); + private final List packagesToDiscover = new ArrayList<>(); + private final List> classesToDiscover = new ArrayList<>(); + private final List methodsToDiscover = new ArrayList<>(); + private final List fieldsToDiscover = new ArrayList<>(); + private final List idsToDiscover = new ArrayList<>(); + + private final List classNameFilters = new ArrayList<>(); + private final List packageNameFilters = new ArrayList<>(); + + @Override + @SuppressWarnings("unchecked") // compatibility is explicitly checked + public List getSelectorsByType(Class selectorType) { + if (ClasspathRootSelector.class.equals(selectorType)) { + return (List) createClasspathRootSelectors(classpathRootsToDiscover); + } + if (PackageSelector.class.equals(selectorType)) { + return (List) createPackageSelectors(packagesToDiscover); + } + if (ClassSelector.class.equals(selectorType)) { + return (List) createClassSelectors(classesToDiscover); + } + if (MethodSelector.class.equals(selectorType)) { + return (List) createMethodSelectors(methodsToDiscover); + } + if (FieldSelector.class.equals(selectorType)) { + return (List) createFieldSelectors(fieldsToDiscover); + } + if (UniqueIdSelector.class.equals(selectorType)) { + return (List) createUniqueIdSelectors(idsToDiscover); + } + return emptyList(); + } + + private List createClasspathRootSelectors(List classpathRootsToDiscover) { + return DiscoverySelectors.selectClasspathRoots(classpathRootsToDiscover.stream().map(Paths::get).collect(toSet())); + } + + private List createPackageSelectors(List packagesToDiscover) { + return packagesToDiscover.stream().map(DiscoverySelectors::selectPackage).collect(toList()); + } + + private List createClassSelectors(List> classesToDiscover) { + return classesToDiscover.stream().map(DiscoverySelectors::selectClass).collect(toList()); + } + + private List createMethodSelectors(List methodsToDiscover) { + return methodsToDiscover.stream().map(m -> selectMethod(m.getDeclaringClass(), m)).collect(toList()); + } + + private List createFieldSelectors(List fieldsToDiscover) { + return fieldsToDiscover.stream().map(f -> selectField(f.getDeclaringClass(), f)).collect(toList()); + } + + private List createUniqueIdSelectors(List idsToDiscover) { + return idsToDiscover.stream().map(DiscoverySelectors::selectUniqueId).collect(toList()); + } + + @Override + @SuppressWarnings("unchecked") // compatibility is explicitly checked + public > List getFiltersByType(Class filterType) { + if (ClassNameFilter.class.equals(filterType)) { + return (List) classNameFilters; + } + if (PackageNameFilter.class.equals(filterType)) { + return (List) packageNameFilters; + } + return emptyList(); + } + + @Override + public ConfigurationParameters getConfigurationParameters() { + return new EmptyConfigurationParameters(); + } + + EngineDiscoveryTestRequest withClasspathRoot(URI uri) { + classpathRootsToDiscover.add(uri); + return this; + } + + EngineDiscoveryTestRequest withPackage(String pkg) { + packagesToDiscover.add(pkg); + return this; + } + + EngineDiscoveryTestRequest withClass(Class clazz) { + classesToDiscover.add(clazz); + return this; + } + + EngineDiscoveryTestRequest withUniqueId(UniqueId id) { + idsToDiscover.add(id); + return this; + } + + EngineDiscoveryTestRequest withClassNameFilter(ClassNameFilter filter) { + classNameFilters.add(filter); + return this; + } + + EngineDiscoveryTestRequest withPackageNameFilter(PackageNameFilter packageNameFilter) { + packageNameFilters.add(packageNameFilter); + return this; + } + + EngineDiscoveryTestRequest withMethod(Class clazz, String methodName) { + try { + methodsToDiscover.add(clazz.getDeclaredMethod(methodName, JavaClasses.class)); + } catch (NoSuchMethodException e) { + throw new RuntimeException(e); + } + return this; + } + + EngineDiscoveryTestRequest withField(Class clazz, String fieldName) { + try { + fieldsToDiscover.add(clazz.getDeclaredField(fieldName)); + } catch (NoSuchFieldException e) { + throw new RuntimeException(e); + } + return this; + } + + private static class EmptyConfigurationParameters implements ConfigurationParameters { + @Override + public Optional get(String key) { + return Optional.empty(); + } + + @Override + public Optional getBoolean(String key) { + return Optional.empty(); + } + + @Override + public Set keySet() { + return emptySet(); + } + } +} diff --git a/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/EngineExecutionTestListener.java b/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/EngineExecutionTestListener.java new file mode 100644 index 0000000000..19f59b34f9 --- /dev/null +++ b/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/EngineExecutionTestListener.java @@ -0,0 +1,160 @@ +package com.tngtech.archunit.junit.internal; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.function.BiConsumer; +import java.util.function.BinaryOperator; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Collector; + +import org.junit.platform.engine.EngineExecutionListener; +import org.junit.platform.engine.TestDescriptor; +import org.junit.platform.engine.TestExecutionResult; +import org.junit.platform.engine.UniqueId; +import org.junit.platform.engine.reporting.ReportEntry; + +import static java.util.stream.Collectors.toList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.platform.engine.TestExecutionResult.Status.FAILED; +import static org.junit.platform.engine.TestExecutionResult.Status.SUCCESSFUL; + +class EngineExecutionTestListener implements EngineExecutionListener { + private final List startedTests = new ArrayList<>(); + private final List finishedTests = new ArrayList<>(); + private final List skippedTests = new ArrayList<>(); + + @Override + public void dynamicTestRegistered(TestDescriptor testDescriptor) { + } + + @Override + public void executionSkipped(TestDescriptor testDescriptor, String reason) { + skippedTests.add(new SkippedTest(testDescriptor, reason)); + } + + @Override + public void executionStarted(TestDescriptor testDescriptor) { + startedTests.add(testDescriptor); + } + + @Override + public void executionFinished(TestDescriptor testDescriptor, TestExecutionResult testExecutionResult) { + finishedTests.add(new FinishedTest(testDescriptor, testExecutionResult)); + } + + @Override + public void reportingEntryPublished(TestDescriptor testDescriptor, ReportEntry entry) { + } + + void verifySuccessful(UniqueId testId) { + verifyStarted(testId); + FinishedTest test = finishedTests.stream().filter(result -> result.hasId(testId)).collect(onlyElement()); + assertThat(test.result.getStatus()).as("Test status of " + test).isEqualTo(SUCCESSFUL); + } + + private void verifyStarted(UniqueId testId) { + boolean testStarted = startedTests.stream().anyMatch(descriptor -> descriptor.getUniqueId().equals(testId)); + assertThat(testStarted).as("Test with id " + testId + " was started").isTrue(); + } + + void verifyViolation(UniqueId testId) { + verifyViolation(testId, ""); + } + + void verifyViolation(UniqueId testId, String messagePart) { + verifyStarted(testId); + FinishedTest test = finishedTests.stream().filter(result -> result.hasId(testId)).collect(onlyElement()); + assertThat(test.result.getStatus()) + .as("Test status of " + test) + .isEqualTo(FAILED); + assertThat(test.result.getThrowable().isPresent()) + .as("Test has thrown Throwable: " + test) + .isTrue(); + assertThat(test.result.getThrowable().get()) + .as("Test Throwable of " + test) + .isInstanceOf(AssertionError.class); + assertThat(test.result.getThrowable().get().getMessage()) + .as("AssertionError message of " + test) + .containsSequence(messagePart); + } + + void verifySkipped(UniqueId testId) { + verifySkipped(testId, ""); + } + + void verifySkipped(UniqueId testId, String reasonPart) { + SkippedTest test = skippedTests.stream().filter(result -> result.hasId(testId)).collect(onlyElement()); + assertThat(test.reason) + .as("reason of " + test) + .containsSequence(reasonPart); + } + + void verifyNoOtherStartExceptHierarchyOf(UniqueId uniqueId) { + List unwanted = startedTests.stream() + .filter(descriptor -> !uniqueId.hasPrefix(descriptor.getUniqueId())) + .collect(toList()); + assertThat(unwanted).as("Unwanted started descriptors").isEmpty(); + } + + private static class FinishedTest { + final TestDescriptor testDescriptor; + final TestExecutionResult result; + + FinishedTest(TestDescriptor testDescriptor, TestExecutionResult result) { + this.testDescriptor = testDescriptor; + this.result = result; + } + + boolean hasId(UniqueId testId) { + return testDescriptor.getUniqueId().equals(testId); + } + + @Override + public String toString() { + return "FinishedTest{" + + "testDescriptor=" + testDescriptor + + ", result=" + result + + '}'; + } + } + + private static class SkippedTest { + final TestDescriptor testDescriptor; + final String reason; + + SkippedTest(TestDescriptor testDescriptor, String reason) { + this.testDescriptor = testDescriptor; + this.reason = reason; + } + + boolean hasId(UniqueId testId) { + return testDescriptor.getUniqueId().equals(testId); + } + + @Override + public String toString() { + return "SkippedTest{" + + "testDescriptor=" + testDescriptor + + ", reason='" + reason + '\'' + + '}'; + } + } + + static Collector, T> onlyElement() { + Supplier> supplier = ArrayList::new; + BiConsumer, T> accumulator = Collection::add; + BinaryOperator> combiner = (left, right) -> { + left.addAll(right); + return left; + }; + Function, T> finisher = collection -> { + if (collection.size() != 1) { + throw new IllegalStateException("Expected collection to have exactly one element, but was " + collection); + } + return collection.iterator().next(); + }; + return Collector.of(supplier, accumulator, combiner, finisher); + } +} diff --git a/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/ClassWithPrivateTests.java b/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/ClassWithPrivateTests.java new file mode 100644 index 0000000000..b81e676ac0 --- /dev/null +++ b/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/ClassWithPrivateTests.java @@ -0,0 +1,20 @@ +package com.tngtech.archunit.junit.internal.testexamples; + +import com.tngtech.archunit.core.domain.JavaClasses; +import com.tngtech.archunit.junit.AnalyzeClasses; +import com.tngtech.archunit.junit.ArchTest; +import com.tngtech.archunit.lang.ArchRule; + +@AnalyzeClasses(packages = "some.dummy.package") +public class ClassWithPrivateTests { + @ArchTest + private final ArchRule privateRuleField = RuleThatFails.on(UnwantedClass.CLASS_VIOLATING_RULES); + + @ArchTest + private void privateRuleMethod(JavaClasses classes) { + RuleThatFails.on(UnwantedClass.CLASS_VIOLATING_RULES).check(classes); + } + + public static final String PRIVATE_RULE_FIELD_NAME = "privateRuleField"; + public static final String PRIVATE_RULE_METHOD_NAME = "privateRuleMethod"; +} diff --git a/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/ComplexMetaTags.java b/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/ComplexMetaTags.java new file mode 100644 index 0000000000..040f5e5028 --- /dev/null +++ b/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/ComplexMetaTags.java @@ -0,0 +1,69 @@ +package com.tngtech.archunit.junit.internal.testexamples; + +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import com.tngtech.archunit.core.domain.JavaClasses; +import com.tngtech.archunit.junit.AnalyzeClasses; +import com.tngtech.archunit.junit.ArchTag; +import com.tngtech.archunit.junit.ArchTest; +import com.tngtech.archunit.junit.ArchTests; +import com.tngtech.archunit.lang.ArchRule; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@ComplexMetaTags.LibraryTag +@AnalyzeClasses +public class ComplexMetaTags { + public static final String FIELD_RULE_NAME = "field_rule"; + public static final String METHOD_RULE_NAME = "method_rule"; + + @RulesTag + @ArchTest + static final ArchTests classWithMetaTag = ArchTests.in(TestClassWithMetaTag.class); + + @RulesTag + @ArchTest + static final ArchTests classWithMetaTags = ArchTests.in(TestClassWithMetaTags.class); + + @FieldTag + @ArchTest + static final ArchRule field_rule = RuleThatFails.on(UnwantedClass.class); + + @MethodTag + @ArchTest + static void method_rule(JavaClasses classes) { + } + + @Inherited + @Retention(RUNTIME) + @Target({TYPE, METHOD, FIELD}) + @ArchTag("library-meta-tag") + @interface LibraryTag { + } + + @Inherited + @Retention(RUNTIME) + @Target({TYPE, METHOD, FIELD}) + @ArchTag("rules-meta-tag") + @interface RulesTag { + } + + @Inherited + @Retention(RUNTIME) + @Target({TYPE, METHOD, FIELD}) + @ArchTag("field-meta-tag") + @interface FieldTag { + } + + @Inherited + @Retention(RUNTIME) + @Target({TYPE, METHOD, FIELD}) + @ArchTag("method-meta-tag") + @interface MethodTag { + } +} diff --git a/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/ComplexRuleLibrary.java b/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/ComplexRuleLibrary.java new file mode 100644 index 0000000000..0173f27326 --- /dev/null +++ b/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/ComplexRuleLibrary.java @@ -0,0 +1,18 @@ +package com.tngtech.archunit.junit.internal.testexamples; + +import com.tngtech.archunit.junit.AnalyzeClasses; +import com.tngtech.archunit.junit.ArchTest; +import com.tngtech.archunit.junit.ArchTests; +import com.tngtech.archunit.junit.internal.testexamples.subtwo.SimpleRules; + +@AnalyzeClasses(packages = "some.dummy.package") +public class ComplexRuleLibrary { + @ArchTest + public static final ArchTests rules_one = ArchTests.in(SimpleRuleLibrary.class); + + @ArchTest + public static final ArchTests rules_two = ArchTests.in(SimpleRules.class); + + public static final String RULES_ONE_FIELD = "rules_one"; + public static final String RULES_TWO_FIELD = "rules_two"; +} diff --git a/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/ComplexTags.java b/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/ComplexTags.java new file mode 100644 index 0000000000..ee058661bd --- /dev/null +++ b/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/ComplexTags.java @@ -0,0 +1,28 @@ +package com.tngtech.archunit.junit.internal.testexamples; + +import com.tngtech.archunit.core.domain.JavaClasses; +import com.tngtech.archunit.junit.AnalyzeClasses; +import com.tngtech.archunit.junit.ArchTag; +import com.tngtech.archunit.junit.ArchTest; +import com.tngtech.archunit.junit.ArchTests; +import com.tngtech.archunit.lang.ArchRule; + +@ArchTag("library-tag") +@AnalyzeClasses +public class ComplexTags { + public static final String FIELD_RULE_NAME = "field_rule"; + public static final String METHOD_RULE_NAME = "method_rule"; + + @ArchTag("rules-tag") + @ArchTest + static final ArchTests classWithTags = ArchTests.in(TestClassWithTags.class); + + @ArchTag("field-tag") + @ArchTest + static final ArchRule field_rule = RuleThatFails.on(UnwantedClass.class); + + @ArchTag("method-tag") + @ArchTest + static void method_rule(JavaClasses classes) { + } +} diff --git a/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/FullAnalyzeClassesSpec.java b/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/FullAnalyzeClassesSpec.java new file mode 100644 index 0000000000..dbaa3cec83 --- /dev/null +++ b/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/FullAnalyzeClassesSpec.java @@ -0,0 +1,49 @@ +package com.tngtech.archunit.junit.internal.testexamples; + +import java.io.File; +import java.util.Collections; +import java.util.Set; + +import com.tngtech.archunit.core.domain.JavaClass; +import com.tngtech.archunit.core.importer.ImportOption.DoNotIncludeJars; +import com.tngtech.archunit.core.importer.ImportOption.DoNotIncludeTests; +import com.tngtech.archunit.core.importer.Location; +import com.tngtech.archunit.junit.AnalyzeClasses; +import com.tngtech.archunit.junit.ArchTest; +import com.tngtech.archunit.junit.LocationProvider; +import com.tngtech.archunit.junit.internal.testexamples.FullAnalyzeClassesSpec.FirstLocationProvider; +import com.tngtech.archunit.junit.internal.testexamples.FullAnalyzeClassesSpec.SecondLocationProvider; +import com.tngtech.archunit.lang.ArchCondition; +import com.tngtech.archunit.lang.ArchRule; +import com.tngtech.archunit.lang.ConditionEvents; + +import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes; + +@AnalyzeClasses( + packages = {"first.pkg", "second.pkg"}, + packagesOf = {Object.class, File.class}, + locations = {FirstLocationProvider.class, SecondLocationProvider.class}, + wholeClasspath = true, + importOptions = {DoNotIncludeTests.class, DoNotIncludeJars.class}) +public class FullAnalyzeClassesSpec { + @ArchTest + public static final ArchRule irrelevant = classes().should(new ArchCondition("exist") { + @Override + public void check(JavaClass item, ConditionEvents events) { + } + }); + + public static class FirstLocationProvider implements LocationProvider { + @Override + public Set get(Class testClass) { + return Collections.emptySet(); + } + } + + public static class SecondLocationProvider implements LocationProvider { + @Override + public Set get(Class testClass) { + return Collections.emptySet(); + } + } +} diff --git a/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/LibraryWithPrivateTests.java b/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/LibraryWithPrivateTests.java new file mode 100644 index 0000000000..8f20282c54 --- /dev/null +++ b/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/LibraryWithPrivateTests.java @@ -0,0 +1,20 @@ +package com.tngtech.archunit.junit.internal.testexamples; + +import com.tngtech.archunit.junit.AnalyzeClasses; +import com.tngtech.archunit.junit.ArchTest; +import com.tngtech.archunit.junit.ArchTests; + +@AnalyzeClasses(packages = "some.dummy.package") +public class LibraryWithPrivateTests { + @ArchTest + private final ArchTests privateRulesField = ArchTests.in(SubRules.class); + + public static final String PRIVATE_RULES_FIELD_NAME = "privateRulesField"; + + public static class SubRules { + @ArchTest + private final ArchTests privateRulesField = ArchTests.in(ClassWithPrivateTests.class); + + public static final String PRIVATE_RULES_FIELD_NAME = "privateRulesField"; + } +} diff --git a/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/RuleThatFails.java b/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/RuleThatFails.java new file mode 100644 index 0000000000..6a9cb62133 --- /dev/null +++ b/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/RuleThatFails.java @@ -0,0 +1,22 @@ +package com.tngtech.archunit.junit.internal.testexamples; + +import com.tngtech.archunit.core.domain.JavaClass; +import com.tngtech.archunit.lang.ArchCondition; +import com.tngtech.archunit.lang.ArchRule; +import com.tngtech.archunit.lang.ConditionEvents; +import com.tngtech.archunit.lang.SimpleConditionEvent; + +import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes; + +public class RuleThatFails { + public static ArchRule on(Class input) { + return classes().should(new ArchCondition("not be " + input.getName()) { + @Override + public void check(JavaClass item, ConditionEvents events) { + if (item.isEquivalentTo(input)) { + events.add(SimpleConditionEvent.violated(item, "Got class " + item.getName())); + } + } + }); + } +} diff --git a/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/SimpleRuleLibrary.java b/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/SimpleRuleLibrary.java new file mode 100644 index 0000000000..e8217f9c90 --- /dev/null +++ b/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/SimpleRuleLibrary.java @@ -0,0 +1,19 @@ +package com.tngtech.archunit.junit.internal.testexamples; + +import com.tngtech.archunit.junit.AnalyzeClasses; +import com.tngtech.archunit.junit.ArchTest; +import com.tngtech.archunit.junit.ArchTests; +import com.tngtech.archunit.junit.internal.testexamples.subone.SimpleRuleField; +import com.tngtech.archunit.junit.internal.testexamples.subtwo.SimpleRules; + +@AnalyzeClasses(packages = "some.dummy.package") +public class SimpleRuleLibrary { + @ArchTest + public static final ArchTests rules_one = ArchTests.in(SimpleRules.class); + + @ArchTest + public static final ArchTests rules_two = ArchTests.in(SimpleRuleField.class); + + public static final String RULES_ONE_FIELD = "rules_one"; + public static final String RULES_TWO_FIELD = "rules_two"; +} diff --git a/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/TestClassWithMetaAnnotationForAnalyzeClasses.java b/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/TestClassWithMetaAnnotationForAnalyzeClasses.java new file mode 100644 index 0000000000..3141f35de0 --- /dev/null +++ b/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/TestClassWithMetaAnnotationForAnalyzeClasses.java @@ -0,0 +1,21 @@ +package com.tngtech.archunit.junit.internal.testexamples; + +import java.lang.annotation.Retention; + +import com.tngtech.archunit.junit.AnalyzeClasses; +import com.tngtech.archunit.junit.ArchTest; +import com.tngtech.archunit.lang.ArchRule; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@TestClassWithMetaAnnotationForAnalyzeClasses.MetaAnalyzeClasses +public class TestClassWithMetaAnnotationForAnalyzeClasses { + + @ArchTest + public static final ArchRule rule_in_class_with_meta_analyze_class_annotation = RuleThatFails.on(UnwantedClass.class); + + @Retention(RUNTIME) + @AnalyzeClasses(wholeClasspath = true) + public @interface MetaAnalyzeClasses { + } +} diff --git a/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/TestClassWithMetaTag.java b/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/TestClassWithMetaTag.java new file mode 100644 index 0000000000..56e5b37b65 --- /dev/null +++ b/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/TestClassWithMetaTag.java @@ -0,0 +1,42 @@ +package com.tngtech.archunit.junit.internal.testexamples; + +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import com.tngtech.archunit.core.domain.JavaClasses; +import com.tngtech.archunit.junit.AnalyzeClasses; +import com.tngtech.archunit.junit.ArchTag; +import com.tngtech.archunit.junit.ArchTest; +import com.tngtech.archunit.junit.ArchTests; +import com.tngtech.archunit.junit.internal.testexamples.subone.SimpleRuleField; +import com.tngtech.archunit.lang.ArchRule; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@TestClassWithMetaTag.MetaTag +@AnalyzeClasses +public class TestClassWithMetaTag { + public static final String FIELD_RULE_NAME = "rule_in_class_with_meta_tag"; + + @ArchTest + public static final ArchRule rule_in_class_with_meta_tag = RuleThatFails.on(UnwantedClass.class); + + @ArchTest + public static final ArchTests rules_in_class_with_meta_tag = ArchTests.in(SimpleRuleField.class); + + @ArchTest + static void method_in_class_with_meta_tag(JavaClasses classes) { + } + + @Inherited + @Retention(RUNTIME) + @Target({TYPE, METHOD, FIELD}) + @ArchTag("meta-tag-one") + @ArchTag("meta-tag-two") + @interface MetaTag { + } +} diff --git a/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/TestClassWithMetaTags.java b/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/TestClassWithMetaTags.java new file mode 100644 index 0000000000..346f2d81f8 --- /dev/null +++ b/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/TestClassWithMetaTags.java @@ -0,0 +1,45 @@ +package com.tngtech.archunit.junit.internal.testexamples; + +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import com.tngtech.archunit.core.domain.JavaClasses; +import com.tngtech.archunit.junit.AnalyzeClasses; +import com.tngtech.archunit.junit.ArchTag; +import com.tngtech.archunit.junit.ArchTags; +import com.tngtech.archunit.junit.ArchTest; +import com.tngtech.archunit.junit.ArchTests; +import com.tngtech.archunit.junit.internal.testexamples.subone.SimpleRuleField; +import com.tngtech.archunit.lang.ArchRule; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@TestClassWithMetaTags.MetaTags +@AnalyzeClasses +public class TestClassWithMetaTags { + public static final String FIELD_RULE_NAME = "rule_in_class_with_meta_tags"; + + @ArchTest + public static final ArchRule rule_in_class_with_meta_tags = RuleThatFails.on(UnwantedClass.class); + + @ArchTest + public static final ArchTests rules_in_class_with_meta_tags = ArchTests.in(SimpleRuleField.class); + + @ArchTest + static void method_in_class_with_meta_tags(JavaClasses classes) { + } + + @Inherited + @Retention(RUNTIME) + @Target({TYPE, METHOD, FIELD}) + @ArchTags({ + @ArchTag("meta-tags-one"), + @ArchTag("meta-tags-two"), + }) + @interface MetaTags { + } +} diff --git a/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/TestClassWithTags.java b/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/TestClassWithTags.java new file mode 100644 index 0000000000..c58c4c0450 --- /dev/null +++ b/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/TestClassWithTags.java @@ -0,0 +1,26 @@ +package com.tngtech.archunit.junit.internal.testexamples; + +import com.tngtech.archunit.core.domain.JavaClasses; +import com.tngtech.archunit.junit.AnalyzeClasses; +import com.tngtech.archunit.junit.ArchTag; +import com.tngtech.archunit.junit.ArchTest; +import com.tngtech.archunit.junit.ArchTests; +import com.tngtech.archunit.junit.internal.testexamples.subone.SimpleRuleField; +import com.tngtech.archunit.lang.ArchRule; + +@ArchTag("tag-one") +@ArchTag("tag-two") +@AnalyzeClasses +public class TestClassWithTags { + public static final String FIELD_RULE_NAME = "rule_in_class_with_tags"; + + @ArchTest + public static final ArchRule rule_in_class_with_tags = RuleThatFails.on(UnwantedClass.class); + + @ArchTest + public static final ArchTests rules_in_class_with_tags = ArchTests.in(SimpleRuleField.class); + + @ArchTest + static void method_in_class_with_tags(JavaClasses classes) { + } +} diff --git a/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/TestFieldWithMetaTag.java b/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/TestFieldWithMetaTag.java new file mode 100644 index 0000000000..9f1a4ad8c9 --- /dev/null +++ b/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/TestFieldWithMetaTag.java @@ -0,0 +1,32 @@ +package com.tngtech.archunit.junit.internal.testexamples; + +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import com.tngtech.archunit.junit.AnalyzeClasses; +import com.tngtech.archunit.junit.ArchTag; +import com.tngtech.archunit.junit.ArchTest; +import com.tngtech.archunit.lang.ArchRule; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@AnalyzeClasses +public class TestFieldWithMetaTag { + public static final String FIELD_WITH_META_TAG_NAME = "field_with_meta_tag"; + + @MetaTag + @ArchTest + static ArchRule field_with_meta_tag = RuleThatFails.on(UnwantedClass.CLASS_VIOLATING_RULES); + + @Inherited + @Retention(RUNTIME) + @Target({TYPE, METHOD, FIELD}) + @ArchTag("field-meta-tag-one") + @ArchTag("field-meta-tag-two") + private @interface MetaTag { + } +} diff --git a/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/TestFieldWithMetaTags.java b/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/TestFieldWithMetaTags.java new file mode 100644 index 0000000000..a5dae5928c --- /dev/null +++ b/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/TestFieldWithMetaTags.java @@ -0,0 +1,35 @@ +package com.tngtech.archunit.junit.internal.testexamples; + +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import com.tngtech.archunit.junit.AnalyzeClasses; +import com.tngtech.archunit.junit.ArchTag; +import com.tngtech.archunit.junit.ArchTags; +import com.tngtech.archunit.junit.ArchTest; +import com.tngtech.archunit.lang.ArchRule; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@AnalyzeClasses +public class TestFieldWithMetaTags { + public static final String FIELD_WITH_META_TAGS_NAME = "field_with_meta_tags"; + + @MetaTags + @ArchTest + static ArchRule field_with_meta_tags = RuleThatFails.on(UnwantedClass.CLASS_VIOLATING_RULES); + + @Inherited + @Retention(RUNTIME) + @Target({TYPE, METHOD, FIELD}) + @ArchTags({ + @ArchTag("field-meta-tags-one"), + @ArchTag("field-meta-tags-two"), + }) + private @interface MetaTags { + } +} diff --git a/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/TestFieldWithTags.java b/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/TestFieldWithTags.java new file mode 100644 index 0000000000..0ed7b4049b --- /dev/null +++ b/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/TestFieldWithTags.java @@ -0,0 +1,16 @@ +package com.tngtech.archunit.junit.internal.testexamples; + +import com.tngtech.archunit.junit.AnalyzeClasses; +import com.tngtech.archunit.junit.ArchTag; +import com.tngtech.archunit.junit.ArchTest; +import com.tngtech.archunit.lang.ArchRule; + +@AnalyzeClasses +public class TestFieldWithTags { + public static final String FIELD_WITH_TAG_NAME = "field_with_tag"; + + @ArchTag("field-tag-one") + @ArchTag("field-tag-two") + @ArchTest + static ArchRule field_with_tag = RuleThatFails.on(UnwantedClass.CLASS_VIOLATING_RULES); +} diff --git a/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/TestMethodWithMetaTag.java b/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/TestMethodWithMetaTag.java new file mode 100644 index 0000000000..e59a626bac --- /dev/null +++ b/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/TestMethodWithMetaTag.java @@ -0,0 +1,33 @@ +package com.tngtech.archunit.junit.internal.testexamples; + +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import com.tngtech.archunit.core.domain.JavaClasses; +import com.tngtech.archunit.junit.AnalyzeClasses; +import com.tngtech.archunit.junit.ArchTag; +import com.tngtech.archunit.junit.ArchTest; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@AnalyzeClasses +public class TestMethodWithMetaTag { + public static final String METHOD_WITH_META_TAG_NAME = "method_with_meta_tag"; + + @MetaTag + @ArchTest + static void method_with_meta_tag(JavaClasses classes) { + } + + @Inherited + @Retention(RUNTIME) + @Target({TYPE, METHOD, FIELD}) + @ArchTag("method-meta-tag-one") + @ArchTag("method-meta-tag-two") + private @interface MetaTag { + } +} diff --git a/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/TestMethodWithMetaTags.java b/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/TestMethodWithMetaTags.java new file mode 100644 index 0000000000..05900c0275 --- /dev/null +++ b/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/TestMethodWithMetaTags.java @@ -0,0 +1,36 @@ +package com.tngtech.archunit.junit.internal.testexamples; + +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import com.tngtech.archunit.core.domain.JavaClasses; +import com.tngtech.archunit.junit.AnalyzeClasses; +import com.tngtech.archunit.junit.ArchTag; +import com.tngtech.archunit.junit.ArchTags; +import com.tngtech.archunit.junit.ArchTest; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@AnalyzeClasses +public class TestMethodWithMetaTags { + public static final String METHOD_WITH_META_TAGS_NAME = "method_with_meta_tags"; + + @MetaTags + @ArchTest + static void method_with_meta_tags(JavaClasses classes) { + } + + @Inherited + @Retention(RUNTIME) + @Target({TYPE, METHOD, FIELD}) + @ArchTags({ + @ArchTag("method-meta-tags-one"), + @ArchTag("method-meta-tags-two"), + }) + private @interface MetaTags { + } +} diff --git a/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/TestMethodWithTags.java b/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/TestMethodWithTags.java new file mode 100644 index 0000000000..e0246a6094 --- /dev/null +++ b/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/TestMethodWithTags.java @@ -0,0 +1,17 @@ +package com.tngtech.archunit.junit.internal.testexamples; + +import com.tngtech.archunit.core.domain.JavaClasses; +import com.tngtech.archunit.junit.AnalyzeClasses; +import com.tngtech.archunit.junit.ArchTag; +import com.tngtech.archunit.junit.ArchTest; + +@AnalyzeClasses +public class TestMethodWithTags { + public static final String METHOD_WITH_TAG_NAME = "method_with_tag"; + + @ArchTag("method-tag-one") + @ArchTag("method-tag-two") + @ArchTest + static void method_with_tag(JavaClasses classes) { + } +} diff --git a/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/UnwantedClass.java b/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/UnwantedClass.java new file mode 100644 index 0000000000..b952454ab1 --- /dev/null +++ b/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/UnwantedClass.java @@ -0,0 +1,6 @@ +package com.tngtech.archunit.junit.internal.testexamples; + +public class UnwantedClass { + public static final Class CLASS_SATISFYING_RULES = Object.class; + public static final Class CLASS_VIOLATING_RULES = UnwantedClass.class; +} diff --git a/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/abstractbase/AbstractBaseClassWithFieldRule.java b/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/abstractbase/AbstractBaseClassWithFieldRule.java new file mode 100644 index 0000000000..b9e4b0e24b --- /dev/null +++ b/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/abstractbase/AbstractBaseClassWithFieldRule.java @@ -0,0 +1,13 @@ +package com.tngtech.archunit.junit.internal.testexamples.abstractbase; + +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; + +public abstract class AbstractBaseClassWithFieldRule { + public static final String INSTANCE_FIELD_NAME = "abstractBaseClassInstanceField"; + + @ArchTest + ArchRule abstractBaseClassInstanceField = RuleThatFails.on(UnwantedClass.CLASS_VIOLATING_RULES); +} diff --git a/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/abstractbase/AbstractBaseClassWithLibraryWithAbstractBaseClass.java b/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/abstractbase/AbstractBaseClassWithLibraryWithAbstractBaseClass.java new file mode 100644 index 0000000000..4fb08ceeb9 --- /dev/null +++ b/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/abstractbase/AbstractBaseClassWithLibraryWithAbstractBaseClass.java @@ -0,0 +1,13 @@ +package com.tngtech.archunit.junit.internal.testexamples.abstractbase; + +import com.tngtech.archunit.junit.ArchTest; +import com.tngtech.archunit.junit.ArchTests; + +public abstract class AbstractBaseClassWithLibraryWithAbstractBaseClass { + public static final String FIELD_RULE_LIBRARY_NAME = "fieldRules"; + public static final String METHOD_RULE_LIBRARY_NAME = "methodRules"; + @ArchTest + ArchTests fieldRules = ArchTests.in(ArchTestWithAbstractBaseClassWithFieldRule.class); + @ArchTest + ArchTests methodRules = ArchTests.in(ArchTestWithAbstractBaseClassWithMethodRule.class); +} diff --git a/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/abstractbase/AbstractBaseClassWithMethodRule.java b/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/abstractbase/AbstractBaseClassWithMethodRule.java new file mode 100644 index 0000000000..b0d03366b4 --- /dev/null +++ b/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/abstractbase/AbstractBaseClassWithMethodRule.java @@ -0,0 +1,15 @@ +package com.tngtech.archunit.junit.internal.testexamples.abstractbase; + +import com.tngtech.archunit.core.domain.JavaClasses; +import com.tngtech.archunit.junit.ArchTest; +import com.tngtech.archunit.junit.internal.testexamples.RuleThatFails; +import com.tngtech.archunit.junit.internal.testexamples.UnwantedClass; + +public abstract class AbstractBaseClassWithMethodRule { + public static final String INSTANCE_METHOD_NAME = "abstractBaseClassInstanceMethod"; + + @ArchTest + void abstractBaseClassInstanceMethod(JavaClasses classes) { + RuleThatFails.on(UnwantedClass.CLASS_VIOLATING_RULES).check(classes); + } +} diff --git a/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/abstractbase/ArchTestWithAbstractBaseClassWithFieldRule.java b/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/abstractbase/ArchTestWithAbstractBaseClassWithFieldRule.java new file mode 100644 index 0000000000..d7f16af74b --- /dev/null +++ b/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/abstractbase/ArchTestWithAbstractBaseClassWithFieldRule.java @@ -0,0 +1,7 @@ +package com.tngtech.archunit.junit.internal.testexamples.abstractbase; + +import com.tngtech.archunit.junit.AnalyzeClasses; + +@AnalyzeClasses(packages = "some.dummy.package") +public class ArchTestWithAbstractBaseClassWithFieldRule extends AbstractBaseClassWithFieldRule { +} diff --git a/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/abstractbase/ArchTestWithAbstractBaseClassWithMethodRule.java b/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/abstractbase/ArchTestWithAbstractBaseClassWithMethodRule.java new file mode 100644 index 0000000000..766b57288e --- /dev/null +++ b/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/abstractbase/ArchTestWithAbstractBaseClassWithMethodRule.java @@ -0,0 +1,7 @@ +package com.tngtech.archunit.junit.internal.testexamples.abstractbase; + +import com.tngtech.archunit.junit.AnalyzeClasses; + +@AnalyzeClasses(packages = "some.dummy.package") +public class ArchTestWithAbstractBaseClassWithMethodRule extends AbstractBaseClassWithMethodRule { +} diff --git a/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/abstractbase/ArchTestWithLibraryWithAbstractBaseClass.java b/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/abstractbase/ArchTestWithLibraryWithAbstractBaseClass.java new file mode 100644 index 0000000000..87beab24e1 --- /dev/null +++ b/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/abstractbase/ArchTestWithLibraryWithAbstractBaseClass.java @@ -0,0 +1,7 @@ +package com.tngtech.archunit.junit.internal.testexamples.abstractbase; + +import com.tngtech.archunit.junit.AnalyzeClasses; + +@AnalyzeClasses(packages = "some.dummy.package") +public class ArchTestWithLibraryWithAbstractBaseClass extends AbstractBaseClassWithLibraryWithAbstractBaseClass { +} diff --git a/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/ignores/ArchIgnoreMetaAnnotation.java b/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/ignores/ArchIgnoreMetaAnnotation.java new file mode 100644 index 0000000000..ed066681dc --- /dev/null +++ b/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/ignores/ArchIgnoreMetaAnnotation.java @@ -0,0 +1,19 @@ +package com.tngtech.archunit.junit.internal.testexamples.ignores; + +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import com.tngtech.archunit.junit.ArchIgnore; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Inherited +@Retention(RUNTIME) +@Target({TYPE, METHOD, FIELD}) +@ArchIgnore(reason = "some example description") +public @interface ArchIgnoreMetaAnnotation { +} diff --git a/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/ignores/IgnoredClass.java b/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/ignores/IgnoredClass.java new file mode 100644 index 0000000000..85e9785fee --- /dev/null +++ b/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/ignores/IgnoredClass.java @@ -0,0 +1,23 @@ +package com.tngtech.archunit.junit.internal.testexamples.ignores; + +import com.tngtech.archunit.core.domain.JavaClasses; +import com.tngtech.archunit.junit.AnalyzeClasses; +import com.tngtech.archunit.junit.ArchIgnore; +import com.tngtech.archunit.junit.ArchTest; +import com.tngtech.archunit.junit.internal.testexamples.RuleThatFails; +import com.tngtech.archunit.lang.ArchRule; + +import static com.tngtech.archunit.junit.internal.testexamples.UnwantedClass.CLASS_VIOLATING_RULES; + +@ArchIgnore +@AnalyzeClasses(packages = "some.dummy.package") +public class IgnoredClass { + + @ArchTest + static final ArchRule rule_one = RuleThatFails.on(CLASS_VIOLATING_RULES); + + @ArchTest + static void rule_two(JavaClasses classes) { + RuleThatFails.on(CLASS_VIOLATING_RULES).check(classes); + } +} diff --git a/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/ignores/IgnoredField.java b/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/ignores/IgnoredField.java new file mode 100644 index 0000000000..fe0dbf6689 --- /dev/null +++ b/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/ignores/IgnoredField.java @@ -0,0 +1,23 @@ +package com.tngtech.archunit.junit.internal.testexamples.ignores; + +import com.tngtech.archunit.junit.AnalyzeClasses; +import com.tngtech.archunit.junit.ArchIgnore; +import com.tngtech.archunit.junit.ArchTest; +import com.tngtech.archunit.junit.internal.testexamples.RuleThatFails; +import com.tngtech.archunit.lang.ArchRule; + +import static com.tngtech.archunit.junit.internal.testexamples.UnwantedClass.CLASS_VIOLATING_RULES; + +@AnalyzeClasses(packages = "some.dummy.package") +public class IgnoredField { + + @ArchTest + static final ArchRule unignored_rule = RuleThatFails.on(CLASS_VIOLATING_RULES); + + @ArchTest + @ArchIgnore(reason = "some example description") + static final ArchRule ignored_rule = RuleThatFails.on(CLASS_VIOLATING_RULES); + + public static final String UNIGNORED_RULE_FIELD = "unignored_rule"; + public static final String IGNORED_RULE_FIELD = "ignored_rule"; +} diff --git a/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/ignores/IgnoredLibrary.java b/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/ignores/IgnoredLibrary.java new file mode 100644 index 0000000000..fcfe2df6fe --- /dev/null +++ b/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/ignores/IgnoredLibrary.java @@ -0,0 +1,25 @@ +package com.tngtech.archunit.junit.internal.testexamples.ignores; + +import com.tngtech.archunit.junit.AnalyzeClasses; +import com.tngtech.archunit.junit.ArchIgnore; +import com.tngtech.archunit.junit.ArchTest; +import com.tngtech.archunit.junit.ArchTests; +import com.tngtech.archunit.junit.internal.testexamples.subtwo.SimpleRules; + +@AnalyzeClasses(packages = "some.dummy.package") +public class IgnoredLibrary { + + @ArchTest + static final ArchTests unignored_lib_one = ArchTests.in(IgnoredClass.class); + + @ArchTest + static final ArchTests unignored_lib_two = ArchTests.in(IgnoredMethod.class); + + @ArchTest + @ArchIgnore + static final ArchTests ignored_lib = ArchTests.in(SimpleRules.class); + + public static final String UNIGNORED_LIB_ONE_FIELD = "unignored_lib_one"; + public static final String UNIGNORED_LIB_TWO_FIELD = "unignored_lib_two"; + public static final String IGNORED_LIB_FIELD = "ignored_lib"; +} diff --git a/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/ignores/IgnoredMethod.java b/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/ignores/IgnoredMethod.java new file mode 100644 index 0000000000..00ecf784cb --- /dev/null +++ b/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/ignores/IgnoredMethod.java @@ -0,0 +1,27 @@ +package com.tngtech.archunit.junit.internal.testexamples.ignores; + +import com.tngtech.archunit.core.domain.JavaClasses; +import com.tngtech.archunit.junit.AnalyzeClasses; +import com.tngtech.archunit.junit.ArchIgnore; +import com.tngtech.archunit.junit.ArchTest; +import com.tngtech.archunit.junit.internal.testexamples.RuleThatFails; + +import static com.tngtech.archunit.junit.internal.testexamples.UnwantedClass.CLASS_VIOLATING_RULES; + +@AnalyzeClasses(packages = "some.dummy.package") +public class IgnoredMethod { + + @ArchTest + static void unignored_rule(JavaClasses classes) { + RuleThatFails.on(CLASS_VIOLATING_RULES).check(classes); + } + + @ArchTest + @ArchIgnore + static void ignored_rule(JavaClasses classes) { + RuleThatFails.on(CLASS_VIOLATING_RULES).check(classes); + } + + public static final String UNIGNORED_RULE_METHOD = "unignored_rule"; + public static final String IGNORED_RULE_METHOD = "ignored_rule"; +} diff --git a/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/ignores/MetaIgnoredClass.java b/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/ignores/MetaIgnoredClass.java new file mode 100644 index 0000000000..29a66c5713 --- /dev/null +++ b/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/ignores/MetaIgnoredClass.java @@ -0,0 +1,22 @@ +package com.tngtech.archunit.junit.internal.testexamples.ignores; + +import com.tngtech.archunit.core.domain.JavaClasses; +import com.tngtech.archunit.junit.AnalyzeClasses; +import com.tngtech.archunit.junit.ArchTest; +import com.tngtech.archunit.junit.internal.testexamples.RuleThatFails; +import com.tngtech.archunit.lang.ArchRule; + +import static com.tngtech.archunit.junit.internal.testexamples.UnwantedClass.CLASS_VIOLATING_RULES; + +@ArchIgnoreMetaAnnotation +@AnalyzeClasses(packages = "some.dummy.package") +public class MetaIgnoredClass { + + @ArchTest + static final ArchRule rule_one = RuleThatFails.on(CLASS_VIOLATING_RULES); + + @ArchTest + static void rule_two(JavaClasses classes) { + RuleThatFails.on(CLASS_VIOLATING_RULES).check(classes); + } +} diff --git a/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/ignores/MetaIgnoredField.java b/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/ignores/MetaIgnoredField.java new file mode 100644 index 0000000000..cbf8f67935 --- /dev/null +++ b/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/ignores/MetaIgnoredField.java @@ -0,0 +1,22 @@ +package com.tngtech.archunit.junit.internal.testexamples.ignores; + +import com.tngtech.archunit.junit.AnalyzeClasses; +import com.tngtech.archunit.junit.ArchTest; +import com.tngtech.archunit.junit.internal.testexamples.RuleThatFails; +import com.tngtech.archunit.lang.ArchRule; + +import static com.tngtech.archunit.junit.internal.testexamples.UnwantedClass.CLASS_VIOLATING_RULES; + +@AnalyzeClasses(packages = "some.dummy.package") +public class MetaIgnoredField { + + @ArchTest + static final ArchRule unignored_rule = RuleThatFails.on(CLASS_VIOLATING_RULES); + + @ArchTest + @ArchIgnoreMetaAnnotation + static final ArchRule ignored_rule = RuleThatFails.on(CLASS_VIOLATING_RULES); + + public static final String UNIGNORED_RULE_FIELD = "unignored_rule"; + public static final String IGNORED_RULE_FIELD = "ignored_rule"; +} diff --git a/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/ignores/MetaIgnoredLibrary.java b/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/ignores/MetaIgnoredLibrary.java new file mode 100644 index 0000000000..e02f7cb896 --- /dev/null +++ b/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/ignores/MetaIgnoredLibrary.java @@ -0,0 +1,24 @@ +package com.tngtech.archunit.junit.internal.testexamples.ignores; + +import com.tngtech.archunit.junit.AnalyzeClasses; +import com.tngtech.archunit.junit.ArchTest; +import com.tngtech.archunit.junit.ArchTests; +import com.tngtech.archunit.junit.internal.testexamples.subtwo.SimpleRules; + +@AnalyzeClasses(packages = "some.dummy.package") +public class MetaIgnoredLibrary { + + @ArchTest + static final ArchTests unignored_lib_one = ArchTests.in(MetaIgnoredClass.class); + + @ArchTest + static final ArchTests unignored_lib_two = ArchTests.in(MetaIgnoredMethod.class); + + @ArchTest + @ArchIgnoreMetaAnnotation + static final ArchTests ignored_lib = ArchTests.in(SimpleRules.class); + + public static final String UNIGNORED_LIB_ONE_FIELD = "unignored_lib_one"; + public static final String UNIGNORED_LIB_TWO_FIELD = "unignored_lib_two"; + public static final String IGNORED_LIB_FIELD = "ignored_lib"; +} diff --git a/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/ignores/MetaIgnoredMethod.java b/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/ignores/MetaIgnoredMethod.java new file mode 100644 index 0000000000..e22f433200 --- /dev/null +++ b/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/ignores/MetaIgnoredMethod.java @@ -0,0 +1,26 @@ +package com.tngtech.archunit.junit.internal.testexamples.ignores; + +import com.tngtech.archunit.core.domain.JavaClasses; +import com.tngtech.archunit.junit.AnalyzeClasses; +import com.tngtech.archunit.junit.ArchTest; +import com.tngtech.archunit.junit.internal.testexamples.RuleThatFails; + +import static com.tngtech.archunit.junit.internal.testexamples.UnwantedClass.CLASS_VIOLATING_RULES; + +@AnalyzeClasses(packages = "some.dummy.package") +public class MetaIgnoredMethod { + + @ArchTest + static void unignored_rule(JavaClasses classes) { + RuleThatFails.on(CLASS_VIOLATING_RULES).check(classes); + } + + @ArchTest + @ArchIgnoreMetaAnnotation + static void ignored_rule(JavaClasses classes) { + RuleThatFails.on(CLASS_VIOLATING_RULES).check(classes); + } + + public static final String UNIGNORED_RULE_METHOD = "unignored_rule"; + public static final String IGNORED_RULE_METHOD = "ignored_rule"; +} diff --git a/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/subone/SimpleRuleField.java b/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/subone/SimpleRuleField.java new file mode 100644 index 0000000000..b5b13f7808 --- /dev/null +++ b/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/subone/SimpleRuleField.java @@ -0,0 +1,15 @@ +package com.tngtech.archunit.junit.internal.testexamples.subone; + +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; + +@AnalyzeClasses(packages = "some.dummy.package") +public class SimpleRuleField { + @ArchTest + public static final ArchRule simple_rule = RuleThatFails.on(UnwantedClass.CLASS_VIOLATING_RULES); + + public static final String SIMPLE_RULE_FIELD_NAME = "simple_rule"; +} diff --git a/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/subone/SimpleRuleMethod.java b/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/subone/SimpleRuleMethod.java new file mode 100644 index 0000000000..957b2d6e37 --- /dev/null +++ b/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/subone/SimpleRuleMethod.java @@ -0,0 +1,17 @@ +package com.tngtech.archunit.junit.internal.testexamples.subone; + +import com.tngtech.archunit.core.domain.JavaClasses; +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; + +@AnalyzeClasses(packages = "some.dummy.package") +public class SimpleRuleMethod { + @ArchTest + static void simple_rule(JavaClasses classes) { + RuleThatFails.on(UnwantedClass.CLASS_VIOLATING_RULES).check(classes); + } + + public static final String SIMPLE_RULE_METHOD_NAME = "simple_rule"; +} diff --git a/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/subtwo/SimpleRules.java b/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/subtwo/SimpleRules.java new file mode 100644 index 0000000000..727f3d60a8 --- /dev/null +++ b/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/subtwo/SimpleRules.java @@ -0,0 +1,37 @@ +package com.tngtech.archunit.junit.internal.testexamples.subtwo; + +import java.util.Set; + +import com.google.common.collect.ImmutableSet; +import com.tngtech.archunit.core.domain.JavaClasses; +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; + +@AnalyzeClasses(packages = "some.dummy.package") +public class SimpleRules { + @ArchTest + public static final ArchRule simple_rule_field_one = RuleThatFails.on(UnwantedClass.CLASS_VIOLATING_RULES); + + @ArchTest + public static final ArchRule simple_rule_field_two = RuleThatFails.on(UnwantedClass.CLASS_VIOLATING_RULES); + + @ArchTest + public static void simple_rule_method_one(JavaClasses classes) { + RuleThatFails.on(UnwantedClass.CLASS_VIOLATING_RULES).check(classes); + } + + @ArchTest + public static void simple_rule_method_two(JavaClasses classes) { + RuleThatFails.on(UnwantedClass.CLASS_VIOLATING_RULES).check(classes); + } + + public static final String SIMPLE_RULE_FIELD_ONE_NAME = "simple_rule_field_one"; + public static final String SIMPLE_RULE_FIELD_TWO_NAME = "simple_rule_field_two"; + public static final Set RULE_FIELD_NAMES = ImmutableSet.of(SIMPLE_RULE_FIELD_ONE_NAME, SIMPLE_RULE_FIELD_TWO_NAME); + public static final String SIMPLE_RULE_METHOD_ONE_NAME = "simple_rule_method_one"; + public static final String SIMPLE_RULE_METHOD_TWO_NAME = "simple_rule_method_two"; + public static final Set RULE_METHOD_NAMES = ImmutableSet.of(SIMPLE_RULE_METHOD_ONE_NAME, SIMPLE_RULE_METHOD_TWO_NAME); +} diff --git a/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/wrong/WrongRuleMethodNotStatic.java b/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/wrong/WrongRuleMethodNotStatic.java new file mode 100644 index 0000000000..a4b448407c --- /dev/null +++ b/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/wrong/WrongRuleMethodNotStatic.java @@ -0,0 +1,14 @@ +package com.tngtech.archunit.junit.internal.testexamples.wrong; + +import com.tngtech.archunit.core.domain.JavaClasses; +import com.tngtech.archunit.junit.AnalyzeClasses; +import com.tngtech.archunit.junit.ArchTest; + +@AnalyzeClasses +public class WrongRuleMethodNotStatic { + @ArchTest + void notStatic(JavaClasses classes) { + } + + public static final String NOT_STATIC_METHOD_NAME = "notStatic"; +} diff --git a/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/wrong/WrongRuleMethodWrongParameters.java b/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/wrong/WrongRuleMethodWrongParameters.java new file mode 100644 index 0000000000..5d3950c553 --- /dev/null +++ b/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/wrong/WrongRuleMethodWrongParameters.java @@ -0,0 +1,13 @@ +package com.tngtech.archunit.junit.internal.testexamples.wrong; + +import com.tngtech.archunit.junit.AnalyzeClasses; +import com.tngtech.archunit.junit.ArchTest; + +@AnalyzeClasses +public class WrongRuleMethodWrongParameters { + @ArchTest + static void wrongParameters() { + } + + public static final String WRONG_PARAMETERS_METHOD_NAME = "wrongParameters"; +} diff --git a/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testutil/LogCaptor.java b/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testutil/LogCaptor.java new file mode 100644 index 0000000000..397fb38aef --- /dev/null +++ b/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testutil/LogCaptor.java @@ -0,0 +1,23 @@ +package com.tngtech.archunit.junit.internal.testutil; + +import java.util.List; + +import com.tngtech.archunit.testutil.TestLogRecorder; +import com.tngtech.archunit.testutil.TestLogRecorder.RecordedLogEvent; +import org.apache.logging.log4j.Level; + +public class LogCaptor { + private final TestLogRecorder logRecorder = new TestLogRecorder(); + + public void watch(Class loggerClass, Level level) { + logRecorder.record(loggerClass, level); + } + + public void cleanUp() { + logRecorder.reset(); + } + + public List getEvents(Level level) { + return logRecorder.getEvents(level); + } +} diff --git a/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testutil/SystemPropertiesExtension.java b/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testutil/SystemPropertiesExtension.java new file mode 100644 index 0000000000..5a865758a9 --- /dev/null +++ b/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testutil/SystemPropertiesExtension.java @@ -0,0 +1,21 @@ +package com.tngtech.archunit.junit.internal.testutil; + +import java.util.Properties; + +import org.junit.jupiter.api.extension.AfterEachCallback; +import org.junit.jupiter.api.extension.BeforeEachCallback; +import org.junit.jupiter.api.extension.ExtensionContext; + +public class SystemPropertiesExtension implements BeforeEachCallback, AfterEachCallback { + private Properties properties; + + @Override + public void beforeEach(ExtensionContext extensionContext) { + properties = (Properties) System.getProperties().clone(); + } + + @Override + public void afterEach(ExtensionContext extensionContext) { + System.setProperties(properties); + } +} diff --git a/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testutil/TestLogExtension.java b/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testutil/TestLogExtension.java new file mode 100644 index 0000000000..a522668335 --- /dev/null +++ b/archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testutil/TestLogExtension.java @@ -0,0 +1,26 @@ +package com.tngtech.archunit.junit.internal.testutil; + +import org.junit.jupiter.api.extension.AfterEachCallback; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.ParameterContext; +import org.junit.jupiter.api.extension.ParameterResolutionException; +import org.junit.jupiter.api.extension.ParameterResolver; + +public class TestLogExtension implements ParameterResolver, AfterEachCallback { + private final LogCaptor logCaptor = new LogCaptor(); + + @Override + public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException { + return LogCaptor.class.equals(parameterContext.getParameter().getType()); + } + + @Override + public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException { + return logCaptor; + } + + @Override + public void afterEach(ExtensionContext context) { + logCaptor.cleanUp(); + } +} diff --git a/archunit-junit/junit6/engine/src/test/resources/archunit.properties b/archunit-junit/junit6/engine/src/test/resources/archunit.properties new file mode 100644 index 0000000000..d24b5e9b45 --- /dev/null +++ b/archunit-junit/junit6/engine/src/test/resources/archunit.properties @@ -0,0 +1,2 @@ +import.dependencyResolutionProcess.maxIterationsForEnclosingTypes=0 +import.dependencyResolutionProcess.maxIterationsForGenericSignatureTypes=0 \ No newline at end of file diff --git a/archunit-maven-test/build.gradle b/archunit-maven-test/build.gradle index 6294c6afed..0e0b9647e8 100644 --- a/archunit-maven-test/build.gradle +++ b/archunit-maven-test/build.gradle @@ -101,6 +101,9 @@ def mavenCommand = { String... params -> ['./mvnw'] + allParams } +def mavenSurefirePlugin = libs.mavenSurefirePlugin.get() +def mavenCompilerPlugin = libs.mavenCompilerPlugin.get() + def addMavenTest = { IntegrationTestConfig config -> String prepareMavenTest = "prepareMavenTest${config.suffix}" String executeRules = "executeRules${config.suffix}" @@ -119,23 +122,25 @@ def addMavenTest = { IntegrationTestConfig config -> '' mavenPom.text = mavenPom.text - .replace('#{archunit.version}', "${version}") - .replace('#{archunitTestArtifact}', config.archunitTestArtifact) - .replace('#{repositories}', repositories) - .replace('#{javaVersion}', "${config.javaVersion}") - .replace('#{additionalDependencies}', config.additionalDependencies ?: '') - .replace('#{surefireExampleConfiguration}', config.surefireExampleConfiguration) - .replaceAll(/#\{(libs|exampleLibs).([^}:]+):?([^}]*)}/) { all, catalog, dependencyReference, scope -> - def versionCatalog = versionCatalogs.named(catalog) - def targetDependency = versionCatalog.findLibrary(dependencyReference).get().get() - def scopePart = scope ? "\n ${scope}" : '' - """ - | - | ${targetDependency.group} - | ${targetDependency.name} - | ${targetDependency.version}${scopePart} - | """.stripMargin().trim() - } + .replace('#{archunit.version}', "${version}") + .replace('#{archunitTestArtifact}', config.archunitTestArtifact) + .replace('#{repositories}', repositories) + .replace('#{javaVersion}', "${config.javaVersion}") + .replace('#{mavenSurefirePluginVersion}', "${mavenSurefirePlugin.version}") + .replace('#{mavenCompilerPluginVersion}', "${mavenCompilerPlugin.version}") + .replace('#{additionalDependencies}', config.additionalDependencies ?: '') + .replace('#{surefireExampleConfiguration}', config.surefireExampleConfiguration) + .replaceAll(/#\{(libs|exampleLibs).([^}:]+):?([^}]*)}/) { all, catalog, dependencyReference, scope -> + def versionCatalog = versionCatalogs.named(catalog) + def targetDependency = versionCatalog.findLibrary(dependencyReference).get().get() + def scopePart = scope ? "\n ${scope}" : '' + """ + | + | ${targetDependency.group} + | ${targetDependency.name} + | ${targetDependency.version}${scopePart} + | """.stripMargin().trim() + } copy { from new File(new File(project(':archunit-example:example-plain').projectDir, 'src'), 'main') @@ -188,6 +193,12 @@ def addMavenTest = { IntegrationTestConfig config -> tasks.create(name: runMavenTest, dependsOn: [prepareMavenTest, executeRules, verifyRules, cleanUpMavenTest]) + [prepareMavenTest, executeRules, verifyRules, cleanUpMavenTest, runMavenTest].each { taskName -> + tasks[taskName].onlyIf("requires Java ${config.minimumJavaVersion} or higher") { + config.supports(integrationTestJavaVersion) + } + } + def executeRulesTask = tasks[executeRules] gradle.projectsEvaluated { def publishTasks = allprojects @@ -198,35 +209,59 @@ def addMavenTest = { IntegrationTestConfig config -> } } -List integrationTestConfigs = [ - new IntegrationTestConfig( - javaVersion: integrationTestJavaVersion, - testType: TestType.PLAIN, - surefireExampleConfiguration: 'com.tngtech.archunit.exampletest.Example', - archunitTestArtifact: 'archunit' - ), - new IntegrationTestConfig( - javaVersion: integrationTestJavaVersion, - testType: TestType.JUNIT4, - surefireExampleConfiguration: 'com.tngtech.archunit.exampletest.junit4.Example', - archunitTestArtifact: 'archunit-junit4' - ) -] +def junit5VintageEngine = libs.junit5VintageEngine.get() +def junit6VintageEngine = libs.junit6VintageEngine.get() -def vintageEngine = libs.junit5VintageEngine.get() -integrationTestConfigs << new IntegrationTestConfig( - javaVersion: integrationTestJavaVersion, - testType: TestType.JUNIT5, - surefireExampleConfiguration: 'example', - archunitTestArtifact: 'archunit-junit5', - additionalDependencies: """ +def testConfigFor = (TestType testType) -> { + switch (testType) { + case TestType.PLAIN: + return new IntegrationTestConfig( + javaVersion: integrationTestJavaVersion, + testType: testType, + surefireExampleConfiguration: 'com.tngtech.archunit.exampletest.Example', + archunitTestArtifact: 'archunit', + ) + case TestType.JUNIT4: + return new IntegrationTestConfig( + javaVersion: integrationTestJavaVersion, + testType: testType, + surefireExampleConfiguration: 'com.tngtech.archunit.exampletest.junit4.Example', + archunitTestArtifact: 'archunit-junit4' + ) + case TestType.JUNIT5: + return new IntegrationTestConfig( + javaVersion: integrationTestJavaVersion, + testType: testType, + surefireExampleConfiguration: 'example', + archunitTestArtifact: 'archunit-junit5', + additionalDependencies: """ - ${vintageEngine.group} - ${vintageEngine.name} - ${vintageEngine.version} + ${junit5VintageEngine.group} + ${junit5VintageEngine.name} + ${junit5VintageEngine.version} test """ -) + ) + case TestType.JUNIT6: + return new IntegrationTestConfig( + javaVersion: integrationTestJavaVersion, + testType: testType, + surefireExampleConfiguration: 'example', + archunitTestArtifact: 'archunit-junit6', + additionalDependencies: """ + + ${junit6VintageEngine.group} + ${junit6VintageEngine.name} + ${junit6VintageEngine.version} + test + """ + ) + default: + throw new GradleException("Test config missing for test type ${testType}") + } +} + +List integrationTestConfigs = TestType.values().toList().stream().map(testConfigFor).toList() integrationTestConfigs.each { config -> project.with(addMavenTest(config)) } @@ -257,10 +292,25 @@ class IntegrationTestConfig { String getSuffix() { testType.name().toLowerCase().capitalize() } + + JavaVersion getMinimumJavaVersion() { + testType.minimumJavaVersion + } + + boolean supports(JavaVersion javaVersion) { + javaVersion.isCompatibleWith(minimumJavaVersion) + } } enum TestType { - PLAIN, - JUNIT4, - JUNIT5 + PLAIN(JavaVersion.VERSION_1_8), + JUNIT4(JavaVersion.VERSION_1_8), + JUNIT5(JavaVersion.VERSION_1_8), + JUNIT6(JavaVersion.VERSION_17) + + final JavaVersion minimumJavaVersion + + TestType(JavaVersion minimumJavaVersion) { + this.minimumJavaVersion = minimumJavaVersion + } } diff --git a/archunit-maven-test/pom.xml.template b/archunit-maven-test/pom.xml.template index 8bdf2fd067..6ed5c6eeaf 100644 --- a/archunit-maven-test/pom.xml.template +++ b/archunit-maven-test/pom.xml.template @@ -70,7 +70,7 @@ org.apache.maven.plugins maven-surefire-plugin - 2.22.2 + #{mavenSurefirePluginVersion} -Xmx2G @@ -78,7 +78,7 @@ org.apache.maven.plugins maven-compiler-plugin - 3.8.1 + #{mavenCompilerPluginVersion} true #{javaVersion} diff --git a/archunit-maven-test/verification/TestResultTest.java b/archunit-maven-test/verification/TestResultTest.java index faddade848..4c24ea3779 100644 --- a/archunit-maven-test/verification/TestResultTest.java +++ b/archunit-maven-test/verification/TestResultTest.java @@ -131,11 +131,16 @@ Set getFailedTests(Match testCaseTags) { Set result = new HashSet<>(); for (int i = 0; i < classNames.size(); i++) { - result.add(new SingleTest(classNames.get(i), testNames.get(i))); + result.add(new SingleTest(toSimpleClassName(classNames.get(i)), testNames.get(i))); } return result; } + private static String toSimpleClassName(String className) { + int packageSeparatorIndex = className.lastIndexOf('.'); + return packageSeparatorIndex >= 0 ? className.substring(packageSeparatorIndex + 1) : className; + } + private void validateAllArchitectureFailures(Match testCaseTags) { assertThat(testCaseTags.find("error")).as("tests in error").isEmpty(); @@ -250,7 +255,7 @@ public Set getTestMethods(Class clazz) { Set result = new HashSet<>(); for (Method method : clazz.getDeclaredMethods()) { if (method.isAnnotationPresent(Test.class)) { - result.add(new SingleTest(method.getDeclaringClass().getName(), method.getName())); + result.add(new SingleTest(method.getDeclaringClass().getSimpleName(), method.getName())); } } return result; @@ -296,7 +301,7 @@ private static SingleTest createSingleTest(List> testClassPath, String testClassPath.stream().skip(1).map(Class::getSimpleName), Stream.of(memberName) ).collect(joining(" > ")); - return new SingleTest(testClassPath.get(0).getName(), testName); + return new SingleTest(testClassPath.get(0).getSimpleName(), testName); } @Override diff --git a/archunit-self-test/build.gradle b/archunit-self-test/build.gradle index 9478a85aed..62e02e3c8f 100644 --- a/archunit-self-test/build.gradle +++ b/archunit-self-test/build.gradle @@ -1,6 +1,5 @@ plugins { id 'archunit.java-conventions' - id 'archunit.java-junit-conventions' } ext.moduleName = 'com.tngtech.archunit.archunit-self-test' @@ -9,14 +8,6 @@ description = "ArchUnit tests for ArchUnit and ArchUnit-Examples themselves" ext.minimumJavaVersion = JavaVersion.VERSION_1_8 dependencies { - implementation project(path: ':archunit') - implementation project(path: ':archunit-junit4') - implementation project(path: ':archunit-junit5') - implementation project(path: ':archunit-junit5-engine') - implementation project(path: ':archunit-example:example-plain') - implementation project(path: ':archunit-example:example-plain', configuration: 'tests') - implementation project(path: ':archunit-example:example-junit4', configuration: 'tests') - implementation project(path: ':archunit-example:example-junit5', configuration: 'tests') - - testImplementation project(path: ':archunit-junit5') + compileOnly project(path: ':archunit') + compileOnly project(path: ':archunit-junit5-api') } diff --git a/archunit-self-test/src/test/java/com/tngtech/archunit/ArchUnitArchitectureTest.java b/archunit-self-test/src/main/java/com/tngtech/archunit/ArchUnitArchitectureRules.java similarity index 73% rename from archunit-self-test/src/test/java/com/tngtech/archunit/ArchUnitArchitectureTest.java rename to archunit-self-test/src/main/java/com/tngtech/archunit/ArchUnitArchitectureRules.java index f2ca1afcec..497dd6b62a 100644 --- a/archunit-self-test/src/test/java/com/tngtech/archunit/ArchUnitArchitectureTest.java +++ b/archunit-self-test/src/main/java/com/tngtech/archunit/ArchUnitArchitectureRules.java @@ -1,9 +1,7 @@ package com.tngtech.archunit; import java.lang.annotation.Annotation; -import java.util.Set; -import com.google.common.collect.ImmutableSet; import com.tngtech.archunit.base.DescribedPredicate; import com.tngtech.archunit.base.MayResolveTypesViaReflection; import com.tngtech.archunit.base.ResolvesTypesViaReflection; @@ -13,16 +11,9 @@ import com.tngtech.archunit.core.domain.JavaClass; import com.tngtech.archunit.core.domain.properties.HasOwner; import com.tngtech.archunit.core.domain.properties.HasOwner.Predicates.With; -import com.tngtech.archunit.core.importer.ImportOption; -import com.tngtech.archunit.core.importer.Location; import com.tngtech.archunit.core.importer.resolvers.ClassResolver; -import com.tngtech.archunit.junit.AnalyzeClasses; -import com.tngtech.archunit.junit.ArchTag; import com.tngtech.archunit.junit.ArchTest; import com.tngtech.archunit.junit.ArchTests; -import com.tngtech.archunit.junit.ArchUnitRunner; -import com.tngtech.archunit.junit.engine_api.FieldSelector; -import com.tngtech.archunit.junit.internal.ArchUnitTestEngine; import com.tngtech.archunit.lang.ArchRule; import static com.tngtech.archunit.base.DescribedPredicate.not; @@ -31,16 +22,18 @@ import static com.tngtech.archunit.core.domain.JavaClass.Predicates.equivalentTo; import static com.tngtech.archunit.core.domain.properties.CanBeAnnotated.Predicates.annotatedWith; import static com.tngtech.archunit.core.domain.properties.HasName.Predicates.name; -import static com.tngtech.archunit.core.importer.ImportOption.Predefined.DO_NOT_INCLUDE_TESTS; import static com.tngtech.archunit.lang.conditions.ArchPredicates.has; import static com.tngtech.archunit.lang.conditions.ArchPredicates.is; import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses; import static com.tngtech.archunit.library.Architectures.layeredArchitecture; -@AnalyzeClasses( - packagesOf = ArchUnitArchitectureTest.class, - importOptions = ArchUnitArchitectureTest.ArchUnitProductionCode.class) -public class ArchUnitArchitectureTest { +/** + * Structural rules that the ArchUnit Core must fulfill. + */ +public class ArchUnitArchitectureRules { + private ArchUnitArchitectureRules() { + } + static final String THIRDPARTY_PACKAGE_IDENTIFIER = "..thirdparty.."; @ArchTest @@ -58,7 +51,7 @@ public class ArchUnitArchitectureTest { .whereLayer("Core").mayOnlyBeAccessedByLayers("Lang", "Library", "JUnit") .whereLayer("Base").mayOnlyBeAccessedByLayers("Root", "Core", "Lang", "Library", "JUnit") - // This is a conscious exception, to allow a more concise API. Otherwise the configuration of the + // This is a conscious exception, to allow a more concise API. Otherwise, the configuration of the // ClassResolver would need to be moved to `importer` making it harder to use, // or we would need to remove the bound from Class, which also makes the // API harder to use @@ -115,33 +108,4 @@ private static DescribedPredicate> classIsResolvedViaReflection() { return defaultClassForName.or(targetIsMarked); } - public static class ArchUnitProductionCode implements ImportOption { - private static final Set SOURCE_ROOTS = sourceRootsOf( - ArchConfiguration.class, - ArchUnitRunner.class, - ArchTag.class, - FieldSelector.class, - ArchUnitTestEngine.class); - - private static Set sourceRootsOf(Class... classes) { - ImmutableSet.Builder result = ImmutableSet.builder(); - for (Class c : classes) { - String classFile = "/" + c.getName().replace('.', '/') + ".class"; - String file = c.getResource(classFile).getFile(); - result.add(file.substring(0, file.indexOf(classFile))); - } - return result.build(); - } - - @Override - public boolean includes(Location location) { - boolean include = false; - for (String sourceRoot : SOURCE_ROOTS) { - if (location.contains(sourceRoot)) { - include = true; - } - } - return include && DO_NOT_INCLUDE_TESTS.includes(location); - } - } } diff --git a/archunit-self-test/src/main/java/com/tngtech/archunit/ArchUnitExampleArchitectureRules.java b/archunit-self-test/src/main/java/com/tngtech/archunit/ArchUnitExampleArchitectureRules.java new file mode 100644 index 0000000000..65c13641a7 --- /dev/null +++ b/archunit-self-test/src/main/java/com/tngtech/archunit/ArchUnitExampleArchitectureRules.java @@ -0,0 +1,13 @@ +package com.tngtech.archunit; + +import com.tngtech.archunit.junit.ArchTest; +import com.tngtech.archunit.lang.ArchRule; + +import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses; + +public class ArchUnitExampleArchitectureRules { + @ArchTest + public static final ArchRule examples_should_be_independent_of_Guava = + noClasses().should().accessClassesThat().resideInAnyPackage("..google..") + .because("we want to keep the dependencies of archunit-example minimal"); +} diff --git a/archunit-self-test/src/main/java/com/tngtech/archunit/ArchitectureTestImportOptions.java b/archunit-self-test/src/main/java/com/tngtech/archunit/ArchitectureTestImportOptions.java new file mode 100644 index 0000000000..f0c909e0ea --- /dev/null +++ b/archunit-self-test/src/main/java/com/tngtech/archunit/ArchitectureTestImportOptions.java @@ -0,0 +1,27 @@ +package com.tngtech.archunit; + +import java.util.regex.Pattern; + +import com.tngtech.archunit.core.importer.ImportOption; +import com.tngtech.archunit.core.importer.Location; + +public final class ArchitectureTestImportOptions { + private ArchitectureTestImportOptions() { + } + + public static String sourceRootOf(Class marker) { + String classFile = "/" + marker.getName().replace('.', '/') + ".class"; + String file = marker.getResource(classFile).getFile(); + return file.substring(0, file.indexOf(classFile)); + } + + public static final class DoNotIncludeSelfTests implements ImportOption { + private static final Pattern ARCH_TEST_OUTPUT = Pattern.compile(".*/build/classes/([^/]+/)?archTest/.*"); + private static final String SOURCE_ROOT = sourceRootOf(ArchitectureTestImportOptions.class); + + @Override + public boolean includes(Location location) { + return !location.contains(SOURCE_ROOT) && !location.matches(ARCH_TEST_OUTPUT); + } + } +} diff --git a/archunit-self-test/src/test/java/com/tngtech/archunit/ImporterRules.java b/archunit-self-test/src/main/java/com/tngtech/archunit/ImporterRules.java similarity index 95% rename from archunit-self-test/src/test/java/com/tngtech/archunit/ImporterRules.java rename to archunit-self-test/src/main/java/com/tngtech/archunit/ImporterRules.java index 50b9709b4b..352cd43dfa 100644 --- a/archunit-self-test/src/test/java/com/tngtech/archunit/ImporterRules.java +++ b/archunit-self-test/src/main/java/com/tngtech/archunit/ImporterRules.java @@ -8,13 +8,16 @@ import com.tngtech.archunit.junit.ArchTest; import com.tngtech.archunit.lang.ArchRule; -import static com.tngtech.archunit.ArchUnitArchitectureTest.THIRDPARTY_PACKAGE_IDENTIFIER; +import static com.tngtech.archunit.ArchUnitArchitectureRules.THIRDPARTY_PACKAGE_IDENTIFIER; import static com.tngtech.archunit.base.DescribedPredicate.doNot; import static com.tngtech.archunit.base.DescribedPredicate.not; import static com.tngtech.archunit.core.domain.JavaClass.Predicates.belongToAnyOf; import static com.tngtech.archunit.core.domain.JavaClass.Predicates.resideOutsideOfPackage; import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses; +/** + * Import rules that the ArchUnit Core must fulfill. + */ public class ImporterRules { @ArchTest diff --git a/archunit-self-test/src/test/java/com/tngtech/archunit/PublicAPIRules.java b/archunit-self-test/src/main/java/com/tngtech/archunit/PublicAPIRules.java similarity index 93% rename from archunit-self-test/src/test/java/com/tngtech/archunit/PublicAPIRules.java rename to archunit-self-test/src/main/java/com/tngtech/archunit/PublicAPIRules.java index ea8a15c7f3..9a2f19b5a5 100644 --- a/archunit-self-test/src/test/java/com/tngtech/archunit/PublicAPIRules.java +++ b/archunit-self-test/src/main/java/com/tngtech/archunit/PublicAPIRules.java @@ -4,7 +4,9 @@ import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.lang.reflect.WildcardType; +import java.util.ArrayList; import java.util.List; +import java.util.Optional; import java.util.Set; import java.util.stream.Stream; @@ -18,7 +20,6 @@ import com.tngtech.archunit.core.domain.JavaMethod; import com.tngtech.archunit.core.domain.properties.HasName; import com.tngtech.archunit.junit.ArchTest; -import com.tngtech.archunit.junit.ArchUnitRunner; import com.tngtech.archunit.lang.ArchCondition; import com.tngtech.archunit.lang.ArchRule; import com.tngtech.archunit.lang.CompositeArchRule; @@ -26,9 +27,7 @@ import com.tngtech.archunit.lang.SimpleConditionEvent; import com.tngtech.archunit.lang.conditions.ArchPredicates; -import static com.google.common.collect.Iterables.getLast; -import static com.google.common.collect.Lists.newArrayList; -import static com.tngtech.archunit.ArchUnitArchitectureTest.THIRDPARTY_PACKAGE_IDENTIFIER; +import static com.tngtech.archunit.ArchUnitArchitectureRules.THIRDPARTY_PACKAGE_IDENTIFIER; import static com.tngtech.archunit.PublicAPI.Usage.INHERITANCE; import static com.tngtech.archunit.base.DescribedPredicate.anyElementThat; import static com.tngtech.archunit.base.DescribedPredicate.doNot; @@ -58,7 +57,13 @@ import static java.util.Arrays.stream; import static java.util.stream.Collectors.toSet; +/** + * Rules that all production code must fulfill. + */ public class PublicAPIRules { + private PublicAPIRules() { + } + @ArchTest public static final ArchRule only_public_API_classes_or_classes_explicitly_marked_as_internal_are_accessible = classes() @@ -69,6 +74,7 @@ public class PublicAPIRules { .and().resideOutsideOfPackage(THIRDPARTY_PACKAGE_IDENTIFIER) .should().notBePublic() + .allowEmptyShould(true) // does not apply to some archunit-junit* packages .as("classes that are not explicitly designed as API should not be public") .because("we risk extensibility and maintainability of ArchUnit, if internal classes leak to users"); @@ -79,7 +85,8 @@ public class PublicAPIRules { .that(haveMemberThatBelongsToPublicApi()) .should(beAnnotatedWith(PublicAPI.class).forSubtype() .or(beAnnotatedWith(Internal.class)) - .or(have(supertype(annotatedWith(PublicAPI.class)).and(not(modifier(PUBLIC)))))); + .or(have(supertype(annotatedWith(PublicAPI.class)).and(not(modifier(PUBLIC)))))) + .allowEmptyShould(true); // does not apply to some archunit-junit* packages @ArchTest public static final ArchRule only_members_that_are_public_API_or_explicitly_marked_as_internal_are_accessible = @@ -89,6 +96,7 @@ public class PublicAPIRules { .and(are(relevantArchUnitMembers())) .should().notBePublic() + .allowEmptyShould(true) // does not apply to some archunit-junit* packages .because("users of ArchUnit should only access intended members, to preserve maintainability"); @@ -97,7 +105,8 @@ public class PublicAPIRules { members() .that().areAnnotatedWith(PublicAPI.class) .and().doNotHaveModifier(SYNTHETIC) - .should(bePubliclyAccessible()); + .should(bePubliclyAccessible()) + .allowEmptyShould(true); // does not apply to some archunit-junit* packages @ArchTest public static final ArchRule all_public_classes_that_are_not_meant_for_inheritance_or_internal_are_final = @@ -107,11 +116,12 @@ public class PublicAPIRules { .and(are(not(internal()))) .and(are(not(enclosedInANonPublicClass()))) .and().resideOutsideOfPackage(THIRDPARTY_PACKAGE_IDENTIFIER) - .and(are(not(equivalentTo(ArchUnitRunner.class)))) + .and().doNotHaveFullyQualifiedName("com.tngtech.archunit.junit.ArchUnitRunner") .should(bePublicAPIForInheritance()) .orShould(beInterfaces()) .orShould().haveModifier(FINAL) + .allowEmptyShould(true) // does not apply to some archunit-junit* packages .as("all public classes not meant for inheritance should be final") .because("users of ArchUnit should only inherit from intended classes, to preserve maintainability"); @@ -125,6 +135,7 @@ public class PublicAPIRules { .and().areNotAnnotatedWith(Internal.class) .and(are(not(onlyUsedAsPublicApiParameter()))) .should().notBePublic() + .allowEmptyShould(true) // does not apply to some archunit-junit* packages .as("Only RuleDefinitions and interfaces within the ArchUnit syntax (..syntax..) should be public"); private static DescribedPredicate onlyUsedAsPublicApiParameter() { @@ -157,7 +168,8 @@ private static DescribedPredicate onlyUsedAsPublicApiParameter() { .and().areDeclaredInClassesThat().areNotAnnotatedWith(Internal.class) .and().arePublic() .and().doNotHaveName("adhereToPlantUmlDiagram") - .should().haveRawParameterTypes(thatArePublic()); + .should().haveRawParameterTypes(thatArePublic()) + .allowEmptyShould(true); // does not apply to some archunit-junit* packages @ArchTest public static final ArchRule predicate_parameters_of_public_API_code_units_should_be_contravariant = @@ -167,6 +179,7 @@ private static DescribedPredicate onlyUsedAsPublicApiParameter() { .and(are(not(declaredIn(annotatedWith(Internal.class))))) .and(ArchPredicates.have(modifier(PUBLIC))) .should(haveContravariantPredicateParameterTypes()) + .allowEmptyShould(true) // does not apply to some archunit-junit* packages .as(String.format( "Public API methods that take a %s should declare the type parameter contravariantly (i.e. %s)", DescribedPredicate.class.getSimpleName(), DescribedPredicate.class.getSimpleName())) @@ -254,10 +267,12 @@ public void check(JavaMember member, ConditionEvents events) { } private Stream getAllEnclosingClasses(JavaMember member) { - List enclosingClasses = newArrayList(member.getOwner()); - while (getLast(enclosingClasses).getEnclosingClass().isPresent()) { - enclosingClasses.add(getLast(enclosingClasses).getEnclosingClass().get()); - } + List enclosingClasses = new ArrayList<>(); + Optional last = Optional.of(member.getOwner()); + do { + enclosingClasses.add(last.get()); + last = last.get().getEnclosingClass(); + } while (last.isPresent()); return enclosingClasses.stream(); } }; diff --git a/archunit-self-test/src/test/java/com/tngtech/archunit/ArchUnitExampleArchitectureTest.java b/archunit-self-test/src/test/java/com/tngtech/archunit/ArchUnitExampleArchitectureTest.java deleted file mode 100644 index 9dc310372f..0000000000 --- a/archunit-self-test/src/test/java/com/tngtech/archunit/ArchUnitExampleArchitectureTest.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.tngtech.archunit; - -import com.tngtech.archunit.ArchUnitExampleArchitectureTest.ArchUnitExampleLocations; -import com.tngtech.archunit.core.importer.ImportOption; -import com.tngtech.archunit.core.importer.Location; -import com.tngtech.archunit.junit.AnalyzeClasses; -import com.tngtech.archunit.junit.ArchTest; -import com.tngtech.archunit.lang.ArchRule; - -import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses; - -@AnalyzeClasses(packages = "com.tngtech.archunit", importOptions = ArchUnitExampleLocations.class) -public class ArchUnitExampleArchitectureTest { - @ArchTest - public static final ArchRule examples_should_be_independent_of_Guava = - noClasses().should().accessClassesThat().resideInAnyPackage("..google..") - .because("we want to keep the dependencies of archunit-example minimal"); - - public static final class ArchUnitExampleLocations implements ImportOption { - @Override - public boolean includes(Location location) { - return location.contains("archunit-example"); - } - } -} diff --git a/archunit/build.gradle b/archunit/build.gradle index 52f3452832..9635ad4b4b 100644 --- a/archunit/build.gradle +++ b/archunit/build.gradle @@ -1,6 +1,7 @@ plugins { id 'archunit.java-release-conventions' id 'archunit.java-junit-conventions' + id 'archunit.self-test-conventions' } ext.moduleName = 'com.tngtech.archunit' @@ -20,6 +21,7 @@ dependencies { exclude module: 'assertj-core' exclude module: 'guava' } + testImplementation project(path: ':archunit-junit5') } shadowJar { diff --git a/archunit/src/archTest/java/com/tngtech/archunit/ArchUnitArchitectureTest.java b/archunit/src/archTest/java/com/tngtech/archunit/ArchUnitArchitectureTest.java new file mode 100644 index 0000000000..cb22695e6d --- /dev/null +++ b/archunit/src/archTest/java/com/tngtech/archunit/ArchUnitArchitectureTest.java @@ -0,0 +1,32 @@ +package com.tngtech.archunit; + +import com.tngtech.archunit.core.importer.ImportOption.DoNotIncludeTests; +import com.tngtech.archunit.core.importer.ImportOption; +import com.tngtech.archunit.core.importer.Location; +import com.tngtech.archunit.junit.AnalyzeClasses; +import com.tngtech.archunit.junit.ArchTest; +import com.tngtech.archunit.junit.ArchTests; + +import static com.tngtech.archunit.ArchitectureTestImportOptions.DoNotIncludeSelfTests; +import static com.tngtech.archunit.ArchitectureTestImportOptions.sourceRootOf; + +@AnalyzeClasses( + packages = "com.tngtech.archunit", + importOptions = {DoNotIncludeTests.class, DoNotIncludeSelfTests.class, ArchUnitArchitectureTest.DoNotIncludeTestResources.class}) +public class ArchUnitArchitectureTest { + private ArchUnitArchitectureTest() { + } + + @ArchTest + public static final ArchTests architecture_rules = ArchTests.in(ArchUnitArchitectureRules.class); + + public static final class DoNotIncludeTestResources implements ImportOption { + private static final String TEST_RESOURCES_ROOT = sourceRootOf(ArchUnitArchitectureTest.class) + .replace("/classes/java/archTest", "/resources/test"); + + @Override + public boolean includes(Location location) { + return !location.contains(TEST_RESOURCES_ROOT); + } + } +} diff --git a/build-steps/release/archunit-examples-utils.gradle b/build-steps/release/archunit-examples-utils.gradle index 7eb3882f99..9b8738c9e4 100644 --- a/build-steps/release/archunit-examples-utils.gradle +++ b/build-steps/release/archunit-examples-utils.gradle @@ -42,7 +42,7 @@ private void updateArchUnitExampleDependencies(File targetArchUnitExampleDir) { } private List updateArchUnitExampleJavaSources(File targetArchUnitExampleDir) { - ['example-plain', 'example-junit4', 'example-junit5'].each { exampleFolder -> + ['example-plain', 'example-junit4', 'example-junit5', 'example-junit6'].each { exampleFolder -> def targetSource = new File(new File(targetArchUnitExampleDir, exampleFolder), 'src') targetSource.deleteDir() copy { diff --git a/build-steps/release/test-release.gradle b/build-steps/release/test-release.gradle index 5cb2489fb1..7024ad9a47 100644 --- a/build-steps/release/test-release.gradle +++ b/build-steps/release/test-release.gradle @@ -59,6 +59,7 @@ task testRelease() { testExampleProject('example-plain') testExampleProject('example-junit4') testExampleProject('example-junit5') + testExampleProject('example-junit6') } } closeSonatypeStagingRepository.finalizedBy(testRelease) diff --git a/build-steps/release/update-examples.gradle b/build-steps/release/update-examples.gradle index ea742a3e28..9d186f82b3 100644 --- a/build-steps/release/update-examples.gradle +++ b/build-steps/release/update-examples.gradle @@ -49,6 +49,7 @@ task removeArchUnitExampleCategory(dependsOn: copyArchUnitExampleSrc) { removeExampleCategory(new File(checkoutDir, 'example-plain'), 'com.tngtech.archunit.exampletest.Example') removeExampleCategory(new File(checkoutDir, 'example-junit4'), 'com.tngtech.archunit.exampletest.junit4.Example') removeExampleTag(new File(checkoutDir, 'example-junit5')) + removeExampleTag(new File(checkoutDir, 'example-junit6')) } } diff --git a/build.gradle b/build.gradle index a19375bcea..aae185c538 100644 --- a/build.gradle +++ b/build.gradle @@ -65,7 +65,11 @@ ext { project(':archunit-junit5-api'), project(':archunit-junit5-engine-api'), project(':archunit-junit5-engine'), - project(':archunit-junit5')] + project(':archunit-junit5'), + project(':archunit-junit6-api'), + project(':archunit-junit6-engine-api'), + project(':archunit-junit6-engine'), + project(':archunit-junit6')] currentScriptRootOf = { it.buildscript.sourceFile.parentFile } scriptRelativePath = { context, fileName -> new File(currentScriptRootOf(context), fileName) } diff --git a/buildSrc/src/main/groovy/archunit.java-release-check-conventions.gradle b/buildSrc/src/main/groovy/archunit.java-release-check-conventions.gradle index 65a2cb8b53..4a97f4b278 100644 --- a/buildSrc/src/main/groovy/archunit.java-release-check-conventions.gradle +++ b/buildSrc/src/main/groovy/archunit.java-release-check-conventions.gradle @@ -20,13 +20,12 @@ task ensureReleaseCheckUpToDate { VersionCatalog versionCatalog = versionCatalogs.named("libs") def dependencies = versionCatalog.libraryAliases.collect { versionCatalog.findLibrary(it).get().get() } def matchingProjectDependency = dependencies.find { - it.group == releaseCheckDependency.groupId && it.name == releaseCheckDependency.artifactId + it.group == releaseCheckDependency.groupId && + it.name == releaseCheckDependency.artifactId && + it.version == releaseCheckDependency.version } assert matchingProjectDependency != null: "No project dependency was found for expected release dependency ${releaseCheckDependency}" - assert matchingProjectDependency.version == releaseCheckDependency.version: - "Release check dependency version ${releaseCheckDependency} doesn't match " + - "project dependency version ${matchingProjectDependency}" } } } diff --git a/buildSrc/src/main/groovy/archunit.self-test-conventions.gradle b/buildSrc/src/main/groovy/archunit.self-test-conventions.gradle new file mode 100644 index 0000000000..340a3ff3fc --- /dev/null +++ b/buildSrc/src/main/groovy/archunit.self-test-conventions.gradle @@ -0,0 +1,29 @@ +sourceSets { + archTest { + java.srcDir 'src/archTest/java' + resources.srcDir 'src/archTest/resources' + compileClasspath += sourceSets.main.output + configurations.testCompileClasspath + runtimeClasspath += output + sourceSets.main.output + configurations.testRuntimeClasspath + } +} + +dependencies { + archTestImplementation project(path: ':archunit-self-test') + // We stay on JUnit5 for the self-tests because of Java 8 compatibility. + archTestImplementation project(path: ':archunit-junit5-api') + archTestRuntimeOnly project(path: ':archunit-junit5-engine') + archTestRuntimeOnly libs.junit5PlatformLauncher +} + +def architectureTest = tasks.register("architectureTest", Test) { + description = 'Runs the shared ArchUnit architecture rules.' + group = 'verification' + testClassesDirs = sourceSets.archTest.output.classesDirs + classpath = sourceSets.archTest.runtimeClasspath + + useJUnitPlatform { + includeEngines 'archunit' + } +} + +check.dependsOn architectureTest diff --git a/buildSrc/src/main/resources/release_check/archunit-junit6-api.pom b/buildSrc/src/main/resources/release_check/archunit-junit6-api.pom new file mode 100644 index 0000000000..b5122a70c8 --- /dev/null +++ b/buildSrc/src/main/resources/release_check/archunit-junit6-api.pom @@ -0,0 +1,52 @@ + + + 4.0.0 + com.tngtech.archunit + archunit-junit6-api + ${archunit.version} + ArchUnit + A Java architecture test library, to specify and assert architecture rules in plain Java - Module 'archunit-junit6-api' + https://github.com/TNG/ArchUnit + + TNG Technology Consulting GmbH + https://www.tngtech.com/ + + + + The Apache Software License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + + codecholeric + Peter Gafert + peter.gafert@archunit.org + + + rweisleder + Roland Weisleder + roland.weisleder@googlemail.com + + + hankem + Manfred Hanke + manfred.hanke@tngtech.com + + + + scm:git@github.com:TNG/ArchUnit.git + scm:git@github.com:TNG/ArchUnit.git + https://github.com/TNG/ArchUnit + + + + com.tngtech.archunit + archunit + ${archunit.version} + compile + + + diff --git a/buildSrc/src/main/resources/release_check/archunit-junit6-engine-api.pom b/buildSrc/src/main/resources/release_check/archunit-junit6-engine-api.pom new file mode 100644 index 0000000000..4b20533162 --- /dev/null +++ b/buildSrc/src/main/resources/release_check/archunit-junit6-engine-api.pom @@ -0,0 +1,53 @@ + + + 4.0.0 + com.tngtech.archunit + archunit-junit6-engine-api + ${archunit.version} + ArchUnit + A Java architecture test library, to specify and assert architecture rules in plain Java - Module 'archunit-junit6-engine-api' + + https://github.com/TNG/ArchUnit + + TNG Technology Consulting GmbH + https://www.tngtech.com/ + + + + The Apache Software License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + + codecholeric + Peter Gafert + peter.gafert@archunit.org + + + rweisleder + Roland Weisleder + roland.weisleder@googlemail.com + + + hankem + Manfred Hanke + manfred.hanke@tngtech.com + + + + scm:git@github.com:TNG/ArchUnit.git + scm:git@github.com:TNG/ArchUnit.git + https://github.com/TNG/ArchUnit + + + + org.junit.platform + junit-platform-engine + 6.1.0 + compile + + + diff --git a/buildSrc/src/main/resources/release_check/archunit-junit6-engine.pom b/buildSrc/src/main/resources/release_check/archunit-junit6-engine.pom new file mode 100644 index 0000000000..4e18fa3a8e --- /dev/null +++ b/buildSrc/src/main/resources/release_check/archunit-junit6-engine.pom @@ -0,0 +1,65 @@ + + + 4.0.0 + com.tngtech.archunit + archunit-junit6-engine + ${archunit.version} + ArchUnit + A Java architecture test library, to specify and assert architecture rules in plain Java - Module 'archunit-junit6-engine' + + https://github.com/TNG/ArchUnit + + TNG Technology Consulting GmbH + https://www.tngtech.com/ + + + + The Apache Software License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + + codecholeric + Peter Gafert + peter.gafert@archunit.org + + + rweisleder + Roland Weisleder + roland.weisleder@googlemail.com + + + hankem + Manfred Hanke + manfred.hanke@tngtech.com + + + + scm:git@github.com:TNG/ArchUnit.git + scm:git@github.com:TNG/ArchUnit.git + https://github.com/TNG/ArchUnit + + + + com.tngtech.archunit + archunit + ${archunit.version} + compile + + + com.tngtech.archunit + archunit-junit6-api + ${archunit.version} + compile + + + com.tngtech.archunit + archunit-junit6-engine-api + ${archunit.version} + compile + + + diff --git a/buildSrc/src/main/resources/release_check/archunit-junit6.pom b/buildSrc/src/main/resources/release_check/archunit-junit6.pom new file mode 100644 index 0000000000..915db4b38d --- /dev/null +++ b/buildSrc/src/main/resources/release_check/archunit-junit6.pom @@ -0,0 +1,59 @@ + + + 4.0.0 + com.tngtech.archunit + archunit-junit6 + ${archunit.version} + ArchUnit + A Java architecture test library, to specify and assert architecture rules in plain Java - Module 'archunit-junit6' + https://github.com/TNG/ArchUnit + + TNG Technology Consulting GmbH + https://www.tngtech.com/ + + + + The Apache Software License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + + codecholeric + Peter Gafert + peter.gafert@archunit.org + + + rweisleder + Roland Weisleder + roland.weisleder@googlemail.com + + + hankem + Manfred Hanke + manfred.hanke@tngtech.com + + + + scm:git@github.com:TNG/ArchUnit.git + scm:git@github.com:TNG/ArchUnit.git + https://github.com/TNG/ArchUnit + + + + com.tngtech.archunit + archunit-junit6-api + ${archunit.version} + compile + + + com.tngtech.archunit + archunit-junit6-engine + ${archunit.version} + runtime + + + diff --git a/docs/userguide/001_Intro.adoc b/docs/userguide/001_Intro.adoc index 0f9e84248c..3055726782 100644 --- a/docs/userguide/001_Intro.adoc +++ b/docs/userguide/001_Intro.adoc @@ -10,9 +10,14 @@ using any plain Java unit testing framework. === Module Overview -ArchUnit consists of the following production modules: `archunit`, `archunit-junit4` as well -as `archunit-junit5-api`, `archunit-junit5-engine` and `archunit-junit5-engine-api`. -Also relevant for end users is the `archunit-example` module. +ArchUnit consists of the following production modules: + +* `archunit` +* `archunit-junit4` +* `archunit-junit5-api`, `archunit-junit5-engine`, `archunit-junit5-engine-api` +* `archunit-junit6-api`, `archunit-junit6-engine`, `archunit-junit6-engine-api` + +The `archunit-example` is also relevant to end users. ==== Module archunit @@ -25,15 +30,15 @@ the domain objects, as well as the rule syntax infrastructure. This module contains the infrastructure to integrate with JUnit 4, in particular the `ArchUnitRunner` to cache imported classes. -==== Modules archunit-junit5-* +==== Modules archunit-junit*-* -These modules contain the infrastructure to integrate with JUnit 5 and contain the respective +These modules contain the infrastructure to integrate with JUnit 5 & 6 and contain the respective infrastructure to cache imported classes between test runs. -`archunit-junit5-api` contains the user API to write tests with ArchUnit's JUnit 5 support, -`archunit-junit5-engine` contains the runtime engine to run those tests. -`archunit-junit5-engine-api` contains API code for tools that want more detailed control -over running ArchUnit JUnit 5 tests, in particular a `FieldSelector` which can be used to -instruct the `ArchUnitTestEngine` to run a specific rule field (compare <>). +`archunit-junit*-api` contains the user API to write tests with ArchUnit's JUnit support. +`archunit-junit*-engine` contains the runtime engine to run those tests. +`archunit-junit*-engine-api` contains API code for tools that want more detailed control +over running ArchUnit JUnit tests, in particular a `FieldSelector` which can be used to +instruct the `ArchUnitTestEngine` to run a specific rule field (compare <>). ==== Module archunit-example diff --git a/docs/userguide/002_Installation.adoc b/docs/userguide/002_Installation.adoc index bc15bf05b5..44f14ef044 100644 --- a/docs/userguide/002_Installation.adoc +++ b/docs/userguide/002_Installation.adoc @@ -2,7 +2,7 @@ To use ArchUnit, it is sufficient to include the respective JAR files in the classpath. Most commonly, this is done by adding the dependency to your dependency management tool, -which is illustrated for Maven and Gradle below. Alternatively you +which is illustrated for Maven and Gradle below. Alternatively, you can obtain the necessary JAR files directly from http://search.maven.org/#search%7Cga%7C1%7Cg%3A%22com.tngtech.archunit%22[Maven Central]. @@ -22,7 +22,7 @@ Maven Central: ---- -[source,options="nowrap",subs="verbatim,attributes"] +[source,groovy,options="nowrap",subs="verbatim,attributes"] .build.gradle ---- dependencies { @@ -30,13 +30,13 @@ dependencies { } ---- -=== JUnit 5 +=== JUnit 5 & 6 -ArchUnit's JUnit 5 artifacts follow the pattern of JUnit Jupiter. There is one artifact containing +ArchUnit's JUnit 5 & 6 artifacts follow the pattern of JUnit Jupiter. There is one artifact containing the API, i.e. the compile time dependencies to write tests. Then there is another artifact containing -the actual `TestEngine` used at runtime. Just like JUnit Jupiter ArchUnit offers one convenience +the actual `TestEngine` used at runtime. Just like JUnit Jupiter, ArchUnit offers one convenience artifact transitively including both API and engine with the correct scope, which in turn can be added -as a test compile dependency. Thus to include ArchUnit's JUnit 5 support, simply add the following dependency +as a test compile dependency. Thus, to include ArchUnit's JUnit 5 & 6 support, simply add the following dependency from Maven Central: [source,xml,options="nowrap",subs="verbatim,attributes"] @@ -44,17 +44,19 @@ from Maven Central: ---- com.tngtech.archunit - archunit-junit5 + + archunit-junit6 {revnumber} test ---- -[source,options="nowrap",subs="verbatim,attributes"] +[source,groovy,options="nowrap",subs="verbatim,attributes"] .build.gradle ---- dependencies { - testImplementation 'com.tngtech.archunit:archunit-junit5:{revnumber}' + // testImplementation 'com.tngtech.archunit:archunit-junit5:{revnumber}' // for JUnit 5 + testImplementation 'com.tngtech.archunit:archunit-junit6:{revnumber}' } ---- @@ -74,7 +76,7 @@ context, include the core ArchUnit dependency from Maven Central: ---- -[source,options="nowrap",subs="verbatim,attributes"] +[source,groovy,options="nowrap",subs="verbatim,attributes"] .build.gradle ---- dependencies { diff --git a/docs/userguide/003_Getting_Started.adoc b/docs/userguide/003_Getting_Started.adoc index c22853ecab..edb5e4698b 100644 --- a/docs/userguide/003_Getting_Started.adoc +++ b/docs/userguide/003_Getting_Started.adoc @@ -6,8 +6,8 @@ Java unit testing framework. To really understand the ideas behind ArchUnit, one === Importing Classes -At its core ArchUnit provides infrastructure to import Java bytecode into Java code structures. -This can be done using the `ClassFileImporter` +At its core, ArchUnit provides infrastructure to import Java bytecode into Java code structures. +This can be done using the `ClassFileImporter`: [source,java,options="nowrap"] ---- @@ -74,10 +74,10 @@ public void Services_should_only_be_accessed_by_Controllers() { } ---- -=== Using JUnit 4 or JUnit 5 +=== Using JUnit While ArchUnit can be used with any unit testing framework, it provides extended support -for writing tests with JUnit 4 and JUnit 5. The main advantage is automatic caching of imported +for writing tests with JUnit. The main advantage is automatic caching of imported classes between tests (of the same imported classes), as well as reduction of boilerplate code. To use the JUnit support, declare ArchUnit's `ArchUnitRunner` (only JUnit 4), declare the classes @@ -85,7 +85,7 @@ to import via `@AnalyzeClasses` and add the respective rules as fields: [source,java,options="nowrap"] ---- -@RunWith(ArchUnitRunner.class) // Remove this line for JUnit 5!! +// @RunWith(ArchUnitRunner.class) // Enable this line for JUnit 4 @AnalyzeClasses(packages = "com.mycompany.myapp") public class MyArchitectureTest { @@ -108,7 +108,7 @@ Using the JUnit support with Kotlin is quite similar to Java: [source,kotlin,options="nowrap"] ---- -@RunWith(ArchUnitRunner::class) // Remove this line for JUnit 5!! +// @RunWith(ArchUnitRunner.class) // Enable this line for JUnit 4 @AnalyzeClasses(packagesOf = [MyArchitectureTest::class]) class MyArchitectureTest { @ArchTest diff --git a/docs/userguide/008_The_Library_API.adoc b/docs/userguide/008_The_Library_API.adoc index 49419c37bd..9b787f404e 100644 --- a/docs/userguide/008_The_Library_API.adoc +++ b/docs/userguide/008_The_Library_API.adoc @@ -285,8 +285,8 @@ where the target class's package doesn't match any declared package identifier i as violation. Note that the `modules()` API can be adjusted in many ways to model custom requirements. -For further details, please take a look at the examples provided -https://github.com/TNG/ArchUnit-Examples/blob/main/example-junit5/src/test/java/com/tngtech/archunit/exampletest/junit5/ModulesTest.java[here]. +For further details, please take a look at the +https://github.com/TNG/ArchUnit-Examples/blob/main/example-junit6/src/test/java/com/tngtech/archunit/exampletest/junit6/ModulesTest.java[examples]. ==== Modularization Core API diff --git a/docs/userguide/009_JUnit_Support.adoc b/docs/userguide/009_JUnit_Support.adoc index 89b7cfd0e0..dbe90de688 100644 --- a/docs/userguide/009_JUnit_Support.adoc +++ b/docs/userguide/009_JUnit_Support.adoc @@ -1,6 +1,6 @@ == JUnit Support -At the moment ArchUnit offers extended support for writing tests with JUnit 4 and JUnit 5. +At the moment, ArchUnit offers extended support for writing tests with JUnit 4, JUnit 5, and JUnit 6. This mainly tackles the problem of caching classes between test runs and to remove some boilerplate. Consider a straight forward approach to write tests: @@ -27,23 +27,23 @@ public void rule2() { ---- For bigger projects, this will have a significant performance impact, since the import can take -a noticeable amount of time. Also rules will always be checked against the imported classes, thus -the explicit call of `check(importedClasses)` is bloat and error prone (i.e. it can be forgotten). +a noticeable amount of time. Furthermore, rules will always be checked against the imported classes, thus +the explicit call of `check(importedClasses)` is bloat and error-prone (i.e. it can be forgotten). -=== JUnit 4 & 5 Support +=== JUnit Integration Make sure you follow the installation instructions at <>, in particular to include the correct dependency for the respective JUnit support. ==== Writing tests -Tests look and behave very similar between JUnit 4 and 5. The only difference is, that with JUnit 4 -it is necessary to add a specific `Runner` to take care of caching and checking rules, while JUnit 5 -picks up the respective `TestEngine` transparently. A test typically looks the following way: +Tests look and behave very similar between JUnit 4 and JUnit 5 & 6. The only difference is that with JUnit 4 +it is necessary to add a specific `Runner` to take care of caching and checking rules, while JUnit 5 & 6 +pick up the respective `TestEngine` transparently. A test typically looks the following way: [source,java,options="nowrap"] ---- -@RunWith(ArchUnitRunner.class) // Remove this line for JUnit 5!! +// @RunWith(ArchUnitRunner.class) // Enable this line for JUnit 4 @AnalyzeClasses(packages = "com.myapp") public class ArchitectureTest { @@ -104,9 +104,9 @@ public class MyLocationProvider implements LocationProvider { @AnalyzeClasses(locations = MyLocationProvider.class) ---- -Furthermore, to choose specific classes beneath those locations, `ImportOption`s can be -specified (compare <>). For example, to import the classpath, but only consider -production code, and only consider code that is directly supplied and does not come from JARs: +Furthermore, in order to choose specific classes beneath those locations, `ImportOption`s can be +specified (compare <>). The following example imports the classpath, but only considers +production code and only considers code that is directly supplied and does not come from JARs: [source,java,options="nowrap"] ---- @@ -182,12 +182,12 @@ public class ArchitectureTest { } ---- -Note for users of JUnit 5: the annotation `@Disabled` has no effect here. +Note for users of JUnit 5 & 6: the annotation `@Disabled` has no effect here. Instead, `@ArchIgnore` should be used. ==== Grouping Rules -Often a project might end up with different categories of rules, for example "service rules" +Often, a project might end up with different categories of rules, for example "service rules" and "persistence rules". It is possible to write one class for each set of rules, and then refer to those sets from another test: @@ -207,7 +207,7 @@ public class PersistenceRules { // further rules } -@RunWith(ArchUnitRunner.class) // Remove this line for JUnit 5!! +// @RunWith(ArchUnitRunner.class) // Enable this line for JUnit 4 @AnalyzeClasses public class ArchitectureTest { @@ -272,4 +272,4 @@ display name generation in ArchUnit with a configuration property (see <