From 1f3704d30535c68728a83ca4ff6b2f9d8558ae7d Mon Sep 17 00:00:00 2001 From: junya koyama Date: Sun, 1 Feb 2026 21:54:57 +0900 Subject: [PATCH] add support JUnit 6.x testing framework initial commit. Issue: #1556 Signed-off-by: junya koyama --- archunit-junit/junit6/aggregator/build.gradle | 16 + archunit-junit/junit6/api/build.gradle | 39 + .../archunit/junit/AnalyzeClasses.java | 90 + .../com/tngtech/archunit/junit/ArchTag.java | 74 + .../com/tngtech/archunit/junit/ArchTags.java | 48 + .../com/tngtech/archunit/junit/ArchTest.java | 54 + archunit-junit/junit6/engine-api/build.gradle | 43 + .../junit/engine_api/FieldSelector.java | 115 ++ .../junit/engine_api/FieldSource.java | 79 + .../junit/engine_api/FieldSelectorTest.java | 48 + archunit-junit/junit6/engine/build.gradle | 63 + .../AbstractArchUnitTestDescriptor.java | 86 + .../internal/ArchUnitEngineDescriptor.java | 26 + .../ArchUnitEngineExecutionContext.java | 21 + ...rchUnitSystemPropertyTestFilterJUnit5.java | 61 + .../internal/ArchUnitTestDescriptor.java | 342 ++++ .../junit/internal/ArchUnitTestEngine.java | 236 +++ .../junit/internal/CreatesChildren.java | 20 + .../junit/internal/ElementResolver.java | 233 +++ .../org.junit.platform.engine.TestEngine | 1 + .../internal/ArchUnitTestEngineTest.java | 1510 +++++++++++++++++ .../internal/EngineDiscoveryTestRequest.java | 175 ++ .../internal/EngineExecutionTestListener.java | 160 ++ .../testexamples/ClassWithPrivateTests.java | 20 + .../testexamples/ComplexMetaTags.java | 69 + .../testexamples/ComplexRuleLibrary.java | 18 + .../internal/testexamples/ComplexTags.java | 28 + .../testexamples/FullAnalyzeClassesSpec.java | 49 + .../testexamples/LibraryWithPrivateTests.java | 20 + .../internal/testexamples/RuleThatFails.java | 22 + .../testexamples/SimpleRuleLibrary.java | 19 + ...ssWithMetaAnnotationForAnalyzeClasses.java | 21 + .../testexamples/TestClassWithMetaTag.java | 42 + .../testexamples/TestClassWithMetaTags.java | 45 + .../testexamples/TestClassWithTags.java | 26 + .../testexamples/TestFieldWithMetaTag.java | 32 + .../testexamples/TestFieldWithMetaTags.java | 35 + .../testexamples/TestFieldWithTags.java | 16 + .../testexamples/TestMethodWithMetaTag.java | 33 + .../testexamples/TestMethodWithMetaTags.java | 36 + .../testexamples/TestMethodWithTags.java | 17 + .../internal/testexamples/UnwantedClass.java | 6 + .../AbstractBaseClassWithFieldRule.java | 13 + ...ClassWithLibraryWithAbstractBaseClass.java | 13 + .../AbstractBaseClassWithMethodRule.java | 15 + ...estWithAbstractBaseClassWithFieldRule.java | 7 + ...stWithAbstractBaseClassWithMethodRule.java | 7 + ...hTestWithLibraryWithAbstractBaseClass.java | 7 + .../ignores/ArchIgnoreMetaAnnotation.java | 19 + .../testexamples/ignores/IgnoredClass.java | 23 + .../testexamples/ignores/IgnoredField.java | 23 + .../testexamples/ignores/IgnoredLibrary.java | 25 + .../testexamples/ignores/IgnoredMethod.java | 27 + .../ignores/MetaIgnoredClass.java | 22 + .../ignores/MetaIgnoredField.java | 22 + .../ignores/MetaIgnoredLibrary.java | 24 + .../ignores/MetaIgnoredMethod.java | 26 + .../testexamples/subone/SimpleRuleField.java | 15 + .../testexamples/subone/SimpleRuleMethod.java | 17 + .../testexamples/subtwo/SimpleRules.java | 37 + .../wrong/WrongRuleMethodNotStatic.java | 14 + .../wrong/WrongRuleMethodWrongParameters.java | 13 + .../junit/internal/testutil/LogCaptor.java | 23 + .../testutil/SystemPropertiesExtension.java | 21 + .../internal/testutil/TestLogExtension.java | 26 + .../src/test/resources/archunit.properties | 2 + build.gradle | 6 +- ...unit.java-release-check-conventions.gradle | 13 +- .../release_check/archunit-junit6-api.pom | 52 + .../archunit-junit6-engine-api.pom | 53 + .../release_check/archunit-junit6-engine.pom | 65 + .../release_check/archunit-junit6.pom | 59 + gradle/libs.versions.toml | 7 + settings.gradle | 5 + 74 files changed, 4793 insertions(+), 2 deletions(-) create mode 100644 archunit-junit/junit6/aggregator/build.gradle create mode 100644 archunit-junit/junit6/api/build.gradle create mode 100644 archunit-junit/junit6/api/src/main/java/com/tngtech/archunit/junit/AnalyzeClasses.java create mode 100644 archunit-junit/junit6/api/src/main/java/com/tngtech/archunit/junit/ArchTag.java create mode 100644 archunit-junit/junit6/api/src/main/java/com/tngtech/archunit/junit/ArchTags.java create mode 100644 archunit-junit/junit6/api/src/main/java/com/tngtech/archunit/junit/ArchTest.java create mode 100644 archunit-junit/junit6/engine-api/build.gradle create mode 100644 archunit-junit/junit6/engine-api/src/main/java/com/tngtech/archunit/junit/engine_api/FieldSelector.java create mode 100644 archunit-junit/junit6/engine-api/src/main/java/com/tngtech/archunit/junit/engine_api/FieldSource.java create mode 100644 archunit-junit/junit6/engine-api/src/test/java/com/tngtech/archunit/junit/engine_api/FieldSelectorTest.java create mode 100644 archunit-junit/junit6/engine/build.gradle create mode 100644 archunit-junit/junit6/engine/src/main/java/com/tngtech/archunit/junit/internal/AbstractArchUnitTestDescriptor.java create mode 100644 archunit-junit/junit6/engine/src/main/java/com/tngtech/archunit/junit/internal/ArchUnitEngineDescriptor.java create mode 100644 archunit-junit/junit6/engine/src/main/java/com/tngtech/archunit/junit/internal/ArchUnitEngineExecutionContext.java create mode 100644 archunit-junit/junit6/engine/src/main/java/com/tngtech/archunit/junit/internal/ArchUnitSystemPropertyTestFilterJUnit5.java create mode 100644 archunit-junit/junit6/engine/src/main/java/com/tngtech/archunit/junit/internal/ArchUnitTestDescriptor.java create mode 100644 archunit-junit/junit6/engine/src/main/java/com/tngtech/archunit/junit/internal/ArchUnitTestEngine.java create mode 100644 archunit-junit/junit6/engine/src/main/java/com/tngtech/archunit/junit/internal/CreatesChildren.java create mode 100644 archunit-junit/junit6/engine/src/main/java/com/tngtech/archunit/junit/internal/ElementResolver.java create mode 100644 archunit-junit/junit6/engine/src/main/resources/META-INF/services/org.junit.platform.engine.TestEngine create mode 100644 archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/ArchUnitTestEngineTest.java create mode 100644 archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/EngineDiscoveryTestRequest.java create mode 100644 archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/EngineExecutionTestListener.java create mode 100644 archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/ClassWithPrivateTests.java create mode 100644 archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/ComplexMetaTags.java create mode 100644 archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/ComplexRuleLibrary.java create mode 100644 archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/ComplexTags.java create mode 100644 archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/FullAnalyzeClassesSpec.java create mode 100644 archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/LibraryWithPrivateTests.java create mode 100644 archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/RuleThatFails.java create mode 100644 archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/SimpleRuleLibrary.java create mode 100644 archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/TestClassWithMetaAnnotationForAnalyzeClasses.java create mode 100644 archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/TestClassWithMetaTag.java create mode 100644 archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/TestClassWithMetaTags.java create mode 100644 archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/TestClassWithTags.java create mode 100644 archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/TestFieldWithMetaTag.java create mode 100644 archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/TestFieldWithMetaTags.java create mode 100644 archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/TestFieldWithTags.java create mode 100644 archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/TestMethodWithMetaTag.java create mode 100644 archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/TestMethodWithMetaTags.java create mode 100644 archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/TestMethodWithTags.java create mode 100644 archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/UnwantedClass.java create mode 100644 archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/abstractbase/AbstractBaseClassWithFieldRule.java create mode 100644 archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/abstractbase/AbstractBaseClassWithLibraryWithAbstractBaseClass.java create mode 100644 archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/abstractbase/AbstractBaseClassWithMethodRule.java create mode 100644 archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/abstractbase/ArchTestWithAbstractBaseClassWithFieldRule.java create mode 100644 archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/abstractbase/ArchTestWithAbstractBaseClassWithMethodRule.java create mode 100644 archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/abstractbase/ArchTestWithLibraryWithAbstractBaseClass.java create mode 100644 archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/ignores/ArchIgnoreMetaAnnotation.java create mode 100644 archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/ignores/IgnoredClass.java create mode 100644 archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/ignores/IgnoredField.java create mode 100644 archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/ignores/IgnoredLibrary.java create mode 100644 archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/ignores/IgnoredMethod.java create mode 100644 archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/ignores/MetaIgnoredClass.java create mode 100644 archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/ignores/MetaIgnoredField.java create mode 100644 archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/ignores/MetaIgnoredLibrary.java create mode 100644 archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/ignores/MetaIgnoredMethod.java create mode 100644 archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/subone/SimpleRuleField.java create mode 100644 archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/subone/SimpleRuleMethod.java create mode 100644 archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/subtwo/SimpleRules.java create mode 100644 archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/wrong/WrongRuleMethodNotStatic.java create mode 100644 archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/wrong/WrongRuleMethodWrongParameters.java create mode 100644 archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testutil/LogCaptor.java create mode 100644 archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testutil/SystemPropertiesExtension.java create mode 100644 archunit-junit/junit6/engine/src/test/java/com/tngtech/archunit/junit/internal/testutil/TestLogExtension.java create mode 100644 archunit-junit/junit6/engine/src/test/resources/archunit.properties create mode 100644 buildSrc/src/main/resources/release_check/archunit-junit6-api.pom create mode 100644 buildSrc/src/main/resources/release_check/archunit-junit6-engine-api.pom create mode 100644 buildSrc/src/main/resources/release_check/archunit-junit6-engine.pom create mode 100644 buildSrc/src/main/resources/release_check/archunit-junit6.pom 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..a26fa4d17b --- /dev/null +++ b/archunit-junit/junit6/api/build.gradle @@ -0,0 +1,39 @@ +plugins { + id 'archunit.java-release-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(dependency { !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/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: + * + * + */ +@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: + * + *
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..90e03fb432 --- /dev/null +++ b/archunit-junit/junit6/engine-api/build.gradle @@ -0,0 +1,43 @@ +plugins { + id 'archunit.java-release-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 + +test { + useJUnitPlatform() { + excludeEngines 'archunit' + } +} + +shadowJar { + exclude 'META-INF/maven/**' + + dependencies { + exclude(dependency { 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/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..59cb3d7234 --- /dev/null +++ b/archunit-junit/junit6/engine/build.gradle @@ -0,0 +1,63 @@ +plugins { + id 'archunit.java-release-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 + 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(dependency { + 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/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..4b5dce2e58 --- /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 5"); + } +} 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/ArchUnitSystemPropertyTestFilterJUnit5.java b/archunit-junit/junit6/engine/src/main/java/com/tngtech/archunit/junit/internal/ArchUnitSystemPropertyTestFilterJUnit5.java new file mode 100644 index 0000000000..1f4bd56d8a --- /dev/null +++ b/archunit-junit/junit6/engine/src/main/java/com/tngtech/archunit/junit/internal/ArchUnitSystemPropertyTestFilterJUnit5.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..67e590d8bb --- /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 5. 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/build.gradle b/build.gradle index e954162853..83fdfca93f 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 ed8a2b516d..b85905bc22 100644 --- a/buildSrc/src/main/groovy/archunit.java-release-check-conventions.gradle +++ b/buildSrc/src/main/groovy/archunit.java-release-check-conventions.gradle @@ -19,9 +19,20 @@ task ensureReleaseCheckUpToDate { releaseCheckDependencies.each { releaseCheckDependency -> VersionCatalog versionCatalog = versionCatalogs.named("libs") def dependencies = versionCatalog.libraryAliases.collect { versionCatalog.findLibrary(it).get().get() } - def matchingProjectDependency = dependencies.find { + def matchingProjectDependencies = dependencies.findAll { it.group == releaseCheckDependency.groupId && it.name == releaseCheckDependency.artifactId } + + assert matchingProjectDependencies.size() == 1 || releaseCheckDependency.groupId.contains("org.junit"): + "Multiple dependency ${matchingProjectDependencies.size()}: ${matchingProjectDependencies}" + + // Filter out duplicate versions of JUnit5 or JUnit6. + def matchingProjectDependency = releaseCheckDependency.groupId.contains("org.junit") + ? releaseCheckDependency.version.contains("6.") + ? matchingProjectDependencies.find { ju -> ju.version.contains("6.") } + : matchingProjectDependencies.find { ju -> !ju.version.contains("6.") } + : matchingProjectDependencies.first() + assert matchingProjectDependency != null: "No project dependency was found for expected release dependency ${releaseCheckDependency}" assert matchingProjectDependency.version == releaseCheckDependency.version: 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..217f201811 --- /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 + 1.14.2 + 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/gradle/libs.versions.toml b/gradle/libs.versions.toml index a6460888a5..2c94d8b686 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,6 +2,7 @@ log4j = "2.25.3" junit5 = "5.14.2" junitPlatform = "1.14.2" # in sync with buildSrc/src/main/resources/release_check/archunit-junit5-engine-api.pom +junit6 = "6.0.2" assertj = "3.27.7" [libraries] @@ -18,6 +19,12 @@ junitPlatform = { group = "org.junit.platform", name = "junit-platform-runner", junitPlatformCommons = { group = "org.junit.platform", name = "junit-platform-commons", version.ref = "junitPlatform" } junitPlatformEngine = { group = "org.junit.platform", name = "junit-platform-engine", version.ref = "junitPlatform" } junitPlatformLauncher = { group = "org.junit.platform", name = "junit-platform-launcher", version.ref = "junitPlatform" } +junit6Jupiter = { group = "org.junit.jupiter", name = "junit-jupiter", version.ref = "junit6" } +junit6VintageEngine = { group = "org.junit.vintage", name = "junit-vintage-engine", version.ref = "junit6" } +junit6Platform = { group = "org.junit.platform", name = "junit-platform-runner", version.ref = "junit6" } +junit6PlatformCommons = { group = "org.junit.platform", name = "junit-platform-commons", version.ref = "junit6" } +junit6PlatformEngine = { group = "org.junit.platform", name = "junit-platform-engine", version.ref = "junit6" } +junit6PlatformLauncher = { group = "org.junit.platform", name = "junit-platform-launcher", version.ref = "junit6" } hamcrest = { group = "org.hamcrest", name = "hamcrest-core", version = "1.3" } junitDataprovider = { group = "com.tngtech.java", name = "junit-dataprovider", version = "1.11.0" } mockito = { group = "org.mockito", name = "mockito-core", version = "4.11.0" } # mockito 5 requires Java 11 diff --git a/settings.gradle b/settings.gradle index 1bb8ade8d8..c4be48665a 100644 --- a/settings.gradle +++ b/settings.gradle @@ -6,6 +6,7 @@ rootProject.name = 'archunit-root' include 'archunit', 'archunit-integration-test', 'archunit-java-modules-test', 'archunit-3rd-party-test', 'archunit-junit', 'archunit-junit4', 'archunit-junit5-api', 'archunit-junit5-engine-api', 'archunit-junit5-engine', 'archunit-junit5', + 'archunit-junit6-api', 'archunit-junit6-engine-api', 'archunit-junit6-engine', 'archunit-junit6', 'archunit-example:example-plain', 'archunit-example:example-junit4', 'archunit-example:example-junit5', 'archunit-maven-test', 'docs' project(':archunit-junit4').projectDir = file('archunit-junit/junit4') @@ -13,6 +14,10 @@ project(':archunit-junit5-api').projectDir = file('archunit-junit/junit5/api') project(':archunit-junit5-engine-api').projectDir = file('archunit-junit/junit5/engine-api') project(':archunit-junit5-engine').projectDir = file('archunit-junit/junit5/engine') project(':archunit-junit5').projectDir = file('archunit-junit/junit5/aggregator') +project(':archunit-junit6-api').projectDir = file('archunit-junit/junit6/api') +project(':archunit-junit6-engine-api').projectDir = file('archunit-junit/junit6/engine-api') +project(':archunit-junit6-engine').projectDir = file('archunit-junit/junit6/engine') +project(':archunit-junit6').projectDir = file('archunit-junit/junit6/aggregator') dependencyResolutionManagement { versionCatalogs {