com.tngtech.archunit.junit.internal.ArchUnitTestEngine 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.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 ArchUnitSystemPropertyTestFilterJUnit5 systemPropertyTestFilter = new ArchUnitSystemPropertyTestFilterJUnit5();
@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();
}
}
}