com.tngtech.archunit.junit.internal.ArchUnitTestDescriptor Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of archunit-junit5-engine Show documentation
Show all versions of archunit-junit5-engine Show documentation
A Java architecture test library, to specify and assert architecture rules in plain Java - Module 'archunit-junit5-engine'
The newest version!
/*
* Copyright 2014-2024 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.google.common.base.Preconditions.checkArgument;
import static com.tngtech.archunit.junit.internal.DisplayNameResolver.determineDisplayName;
import static com.tngtech.archunit.junit.internal.ReflectionUtils.getAllFields;
import static com.tngtech.archunit.junit.internal.ReflectionUtils.getAllMethods;
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.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 (clazz.getAnnotation(AnalyzeClasses.class) == null) {
LOG.warn("Class {} is not annotated with @{} and thus cannot run as a top level test. "
+ "This warning can be ignored if {} is only used as part of a rules library included via {}.in({}.class).",
clazz.getName(), AnalyzeClasses.class.getSimpleName(),
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 JUnit5ClassAnalysisRequest(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 super Field> doWithField) {
getAllFields(archTests.getDefinitionLocation(), withAnnotation(ArchTest.class)).forEach(doWithField);
}
void handleMethods(Consumer super Method> doWithMethod) {
getAllMethods(archTests.getDefinitionLocation(), withAnnotation(ArchTest.class)).forEach(doWithMethod);
}
}
private static class JUnit5ClassAnalysisRequest implements ClassAnalysisRequest {
private final AnalyzeClasses analyzeClasses;
JUnit5ClassAnalysisRequest(Class> testClass) {
analyzeClasses = checkAnnotation(testClass);
}
private static AnalyzeClasses checkAnnotation(Class> testClass) {
AnalyzeClasses analyzeClasses = testClass.getAnnotation(AnalyzeClasses.class);
checkArgument(analyzeClasses != null,
"Class %s must be annotated with @%s",
testClass.getSimpleName(), AnalyzeClasses.class.getSimpleName());
return analyzeClasses;
}
@Override
public String[] getPackageNames() {
return analyzeClasses.packages();
}
@Override
public Class>[] getPackageRoots() {
return analyzeClasses.packagesOf();
}
@Override
public Class extends LocationProvider>[] getLocationProviders() {
return analyzeClasses.locations();
}
@Override
public Class extends ImportOption>[] 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();
}
}
}