From 0f81cb3e5e31760ea580bbe4a0e502efe089a87e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20Sierszen=CC=81?= Date: Thu, 23 Jun 2022 10:14:29 +0200 Subject: [PATCH 1/3] Base filtering mechanism --- .../junit/internal/ArchTestExecution.java | 16 +++ .../internal/ArchTestMethodExecution.java | 3 + archunit-junit/junit5/engine/build.gradle | 4 + .../internal/ArchUnitEngineDescriptor.java | 11 ++ .../internal/ArchUnitTestDescriptor.java | 63 +++++++---- .../junit/internal/ArchUnitTestEngine.java | 51 ++++++++- .../junit/internal/CreatesChildren.java | 4 +- .../filtering/AbstractTestNameFilter.java | 105 ++++++++++++++++++ .../filtering/FieldSelectorFactory.java | 50 +++++++++ .../filtering/MethodSelectorFactory.java | 50 +++++++++ .../internal/filtering/ReflectionUtils.java | 61 ++++++++++ .../filtering/TestSelectorFactory.java | 88 +++++++++++++++ .../internal/filtering/TestSourceFilter.java | 37 ++++++ .../junit/internal/ReflectionUtils.java | 37 +++++- archunit-tooling-test/build.gradle | 1 - .../tooling/engines/gradle/GradleEngine.java | 5 +- .../engines/surefire/MavenSurefireEngine.java | 24 +--- .../tooling/utils/JUnitEngineResolver.java | 27 +++++ .../resources/project/build.gradle | 4 +- gradle/wrapper/gradle-wrapper.properties | 2 +- 20 files changed, 583 insertions(+), 60 deletions(-) create mode 100644 archunit-junit/junit5/engine/src/main/java/com/tngtech/archunit/junit/internal/filtering/AbstractTestNameFilter.java create mode 100644 archunit-junit/junit5/engine/src/main/java/com/tngtech/archunit/junit/internal/filtering/FieldSelectorFactory.java create mode 100644 archunit-junit/junit5/engine/src/main/java/com/tngtech/archunit/junit/internal/filtering/MethodSelectorFactory.java create mode 100644 archunit-junit/junit5/engine/src/main/java/com/tngtech/archunit/junit/internal/filtering/ReflectionUtils.java create mode 100644 archunit-junit/junit5/engine/src/main/java/com/tngtech/archunit/junit/internal/filtering/TestSelectorFactory.java create mode 100644 archunit-junit/junit5/engine/src/main/java/com/tngtech/archunit/junit/internal/filtering/TestSourceFilter.java create mode 100644 archunit-tooling-test/src/main/java/com/tngtech/archunit/tooling/utils/JUnitEngineResolver.java diff --git a/archunit-junit/junit4/src/main/java/com/tngtech/archunit/junit/internal/ArchTestExecution.java b/archunit-junit/junit4/src/main/java/com/tngtech/archunit/junit/internal/ArchTestExecution.java index 51372bc049..fa71392795 100644 --- a/archunit-junit/junit4/src/main/java/com/tngtech/archunit/junit/internal/ArchTestExecution.java +++ b/archunit-junit/junit4/src/main/java/com/tngtech/archunit/junit/internal/ArchTestExecution.java @@ -18,6 +18,7 @@ import java.lang.reflect.Field; import com.tngtech.archunit.core.domain.JavaClasses; +import org.junit.AssumptionViolatedException; import org.junit.runner.Description; import org.junit.runner.notification.Failure; import org.junit.runner.notification.RunNotifier; @@ -77,4 +78,19 @@ void notify(RunNotifier notifier) { notifier.fireTestFailure(new Failure(description, failure)); } } + + static class AbortedResult extends Result { + private final Description description; + private final AssumptionViolatedException failedAssumption; + + AbortedResult(Description description, AssumptionViolatedException failedAssumption) { + this.description = description; + this.failedAssumption = failedAssumption; + } + + @Override + void notify(RunNotifier notifier) { + notifier.fireTestAssumptionFailed(new Failure(description, failedAssumption)); + } + } } diff --git a/archunit-junit/junit4/src/main/java/com/tngtech/archunit/junit/internal/ArchTestMethodExecution.java b/archunit-junit/junit4/src/main/java/com/tngtech/archunit/junit/internal/ArchTestMethodExecution.java index 2fdda33f16..c3b5583108 100644 --- a/archunit-junit/junit4/src/main/java/com/tngtech/archunit/junit/internal/ArchTestMethodExecution.java +++ b/archunit-junit/junit4/src/main/java/com/tngtech/archunit/junit/internal/ArchTestMethodExecution.java @@ -20,6 +20,7 @@ import com.tngtech.archunit.core.domain.JavaClasses; import com.tngtech.archunit.junit.ArchTest; +import org.junit.AssumptionViolatedException; import org.junit.runner.Description; import static com.tngtech.archunit.junit.internal.DisplayNameResolver.determineDisplayName; @@ -38,6 +39,8 @@ Result evaluateOn(JavaClasses classes) { try { executeTestMethod(classes); return new PositiveResult(); + } catch (AssumptionViolatedException failedAssumption) { + return new AbortedResult(describeSelf(), failedAssumption); } catch (Throwable failure) { return new NegativeResult(describeSelf(), failure); } diff --git a/archunit-junit/junit5/engine/build.gradle b/archunit-junit/junit5/engine/build.gradle index 58abbe8154..b2f668a6dc 100644 --- a/archunit-junit/junit5/engine/build.gradle +++ b/archunit-junit/junit5/engine/build.gradle @@ -10,9 +10,13 @@ dependencies { api project(path: ':archunit') api project(path: ':archunit-junit5-api') api project(path: ':archunit-junit5-engine-api') + implementation project(path: ':archunit-junit') dependency.addGuava { dependencyNotation, config -> implementation(dependencyNotation, config) } implementation dependency.slf4j + implementation dependency.junitPlatformLauncher + + compileOnly dependency.findBugsAnnotations testImplementation project(path: ':archunit', configuration: 'tests') testImplementation dependency.assertj diff --git a/archunit-junit/junit5/engine/src/main/java/com/tngtech/archunit/junit/internal/ArchUnitEngineDescriptor.java b/archunit-junit/junit5/engine/src/main/java/com/tngtech/archunit/junit/internal/ArchUnitEngineDescriptor.java index 8981ccee06..5e534e46c4 100644 --- a/archunit-junit/junit5/engine/src/main/java/com/tngtech/archunit/junit/internal/ArchUnitEngineDescriptor.java +++ b/archunit-junit/junit5/engine/src/main/java/com/tngtech/archunit/junit/internal/ArchUnitEngineDescriptor.java @@ -15,6 +15,7 @@ */ package com.tngtech.archunit.junit.internal; +import com.tngtech.archunit.junit.internal.filtering.TestSourceFilter; import org.junit.platform.engine.UniqueId; import org.junit.platform.engine.support.descriptor.EngineDescriptor; import org.junit.platform.engine.support.hierarchical.Node; @@ -23,4 +24,14 @@ class ArchUnitEngineDescriptor extends EngineDescriptor implements Node testClass, Cla this.classCache = classCache; } - static void resolve(TestDescriptor parent, ElementResolver resolver, ClassCache classCache) { + static void resolve(TestDescriptor parent, ElementResolver resolver, ClassCache classCache, TestSourceFilter additionalFilter) { resolver.resolveClass() - .ifRequestedAndResolved(CreatesChildren::createChildren) - .ifRequestedButUnresolved((clazz, childResolver) -> createTestDescriptor(parent, classCache, clazz, childResolver)); + .ifRequestedAndResolved((resolvedMember, elementResolver) -> resolvedMember.createChildren(resolver, additionalFilter)) + .ifRequestedButUnresolved((clazz, childResolver) -> createTestDescriptor(parent, classCache, clazz, childResolver, additionalFilter)); } - private static void createTestDescriptor(TestDescriptor parent, ClassCache classCache, Class clazz, ElementResolver childResolver) { + private static void createTestDescriptor(TestDescriptor parent, ClassCache classCache, Class clazz, ElementResolver childResolver, + TestSourceFilter additionalFilter) { 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).", @@ -79,36 +81,44 @@ private static void createTestDescriptor(TestDescriptor parent, ClassCache class ArchUnitTestDescriptor classDescriptor = new ArchUnitTestDescriptor(childResolver, clazz, classCache); parent.addChild(classDescriptor); - classDescriptor.createChildren(childResolver); + classDescriptor.createChildren(childResolver, additionalFilter); } @Override - public void createChildren(ElementResolver resolver) { + public void createChildren(ElementResolver resolver, TestSourceFilter filter) { Supplier classes = () -> classCache.getClassesToAnalyzeFor(testClass, new JUnit5ClassAnalysisRequest(testClass)); getAllFields(testClass, withAnnotation(ArchTest.class)) - .forEach(field -> resolveField(resolver, classes, field)); + .forEach(field -> resolveField(resolver, classes, field, filter)); getAllMethods(testClass, withAnnotation(ArchTest.class)) - .forEach(method -> resolveMethod(resolver, classes, method)); + .forEach(method -> resolveMethod(resolver, classes, method, filter)); } - private void resolveField(ElementResolver resolver, Supplier classes, Field field) { + private void resolveField(ElementResolver resolver, Supplier classes, Field field, TestSourceFilter filter) { resolver.resolveField(field) - .ifUnresolved(childResolver -> resolveChildren(this, childResolver, field, classes)); + .ifUnresolved(childResolver -> resolveChildren(this, childResolver, field, classes, filter)); } - private void resolveMethod(ElementResolver resolver, Supplier classes, Method method) { + private void resolveMethod(ElementResolver resolver, Supplier classes, Method method, TestSourceFilter filter) { resolver.resolveMethod(method) - .ifUnresolved(childResolver -> addChild(new ArchUnitMethodDescriptor(getUniqueId(), method, classes))); + .ifUnresolved(childResolver -> { + ArchUnitMethodDescriptor descriptor = new ArchUnitMethodDescriptor(getUniqueId(), method, classes); + if (filter.shouldRun(descriptor)) { + addChild(descriptor); + } + }); } private static void resolveChildren( - TestDescriptor parent, ElementResolver resolver, Field field, Supplier classes) { + TestDescriptor parent, ElementResolver resolver, Field field, Supplier classes, TestSourceFilter filter) { if (ArchTests.class.isAssignableFrom(field.getType())) { - resolveArchRules(parent, resolver, field, classes); + resolveArchRules(parent, resolver, field, classes, filter); } else { - parent.addChild(new ArchUnitRuleDescriptor(resolver.getUniqueId(), getValue(field), classes, field)); + ArchUnitRuleDescriptor descriptor = new ArchUnitRuleDescriptor(resolver.getUniqueId(), getValue(field), classes, field); + if (filter.shouldRun(descriptor)) { + parent.addChild(descriptor); + } } } @@ -117,16 +127,18 @@ private static T getValue(Field field) { } private static void resolveArchRules( - TestDescriptor parent, ElementResolver resolver, Field field, Supplier classes) { + TestDescriptor parent, ElementResolver resolver, Field field, Supplier classes, TestSourceFilter filter) { + if (!filter.shouldRun(FieldSource.from(field))) { + return; + } DeclaredArchTests archTests = getDeclaredArchTests(field); - resolver.resolveClass(archTests.getDefinitionLocation()) - .ifRequestedAndResolved(CreatesChildren::createChildren) + .ifRequestedAndResolved((resolvedMember, elementResolver) -> resolvedMember.createChildren(resolver, filter)) .ifRequestedButUnresolved((clazz, childResolver) -> { ArchUnitArchTestsDescriptor rulesDescriptor = new ArchUnitArchTestsDescriptor(childResolver, archTests, classes, field); parent.addChild(rulesDescriptor); - rulesDescriptor.createChildren(childResolver); + rulesDescriptor.createChildren(childResolver, TestSourceFilter.NOOP); }); } @@ -194,6 +206,7 @@ public Type getType() { @Override public ArchUnitEngineExecutionContext execute(ArchUnitEngineExecutionContext context, DynamicTestExecutor dynamicTestExecutor) { + invokeMethod(method, method.getDeclaringClass(), classes.get()); return context; } @@ -215,14 +228,18 @@ private static class ArchUnitArchTestsDescriptor extends AbstractArchUnitTestDes } @Override - public void createChildren(ElementResolver resolver) { + public void createChildren(ElementResolver resolver, TestSourceFilter filter) { archTests.handleFields(field -> resolver.resolve(FIELD_SEGMENT_TYPE, field.getName(), childResolver -> - resolveChildren(this, childResolver, field, classes))); + resolveChildren(this, childResolver, field, classes, filter))); archTests.handleMethods(method -> - resolver.resolve(METHOD_SEGMENT_TYPE, method.getName(), childResolver -> - addChild(new ArchUnitMethodDescriptor(getUniqueId(), method, classes)))); + resolver.resolve(METHOD_SEGMENT_TYPE, method.getName(), childResolver -> { + ArchUnitMethodDescriptor descriptor = new ArchUnitMethodDescriptor(getUniqueId(), method, classes); + if (filter.shouldRun(descriptor)) { + addChild(descriptor); + } + })); } @Override diff --git a/archunit-junit/junit5/engine/src/main/java/com/tngtech/archunit/junit/internal/ArchUnitTestEngine.java b/archunit-junit/junit5/engine/src/main/java/com/tngtech/archunit/junit/internal/ArchUnitTestEngine.java index 0d426bc961..475c133eab 100644 --- a/archunit-junit/junit5/engine/src/main/java/com/tngtech/archunit/junit/internal/ArchUnitTestEngine.java +++ b/archunit-junit/junit5/engine/src/main/java/com/tngtech/archunit/junit/internal/ArchUnitTestEngine.java @@ -18,9 +18,12 @@ import java.net.MalformedURLException; import java.net.URI; import java.net.URL; +import java.util.Arrays; +import java.util.Collection; import java.util.List; import java.util.Optional; import java.util.function.Predicate; +import java.util.stream.Collectors; import java.util.stream.Stream; import com.tngtech.archunit.Internal; @@ -30,6 +33,7 @@ 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.internal.filtering.TestSourceFilter; import org.junit.platform.engine.EngineDiscoveryRequest; import org.junit.platform.engine.ExecutionRequest; import org.junit.platform.engine.Filter; @@ -43,6 +47,7 @@ import org.junit.platform.engine.discovery.PackageSelector; import org.junit.platform.engine.discovery.UniqueIdSelector; import org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine; +import org.junit.platform.engine.support.hierarchical.ThrowableCollector; import static com.tngtech.archunit.junit.internal.ReflectionUtils.getAllFields; import static com.tngtech.archunit.junit.internal.ReflectionUtils.getAllMethods; @@ -67,6 +72,22 @@ public final class ArchUnitTestEngine extends HierarchicalTestEngine { static final String UNIQUE_ID = "archunit"; + private static final Collection ABORTING_THROWABLE_NAMES = Arrays.asList( + "org.junit.internal.AssumptionViolatedException", + "org.junit.AssumptionViolatedException", + "org.opentest4j.TestAbortedException" + ); + + private final Collection> abortingThrowables; + + public ArchUnitTestEngine() { + abortingThrowables = ABORTING_THROWABLE_NAMES.stream() + .map(this::maybeLoadClass) + .filter(Optional::isPresent) + .map(Optional::get) + .collect(Collectors.toSet()); + } + private SharedCache cache = new SharedCache(); // NOTE: We want to change this in tests -> no static/final reference @Override @@ -77,6 +98,7 @@ public String getId() { @Override public TestDescriptor discover(EngineDiscoveryRequest discoveryRequest, UniqueId uniqueId) { ArchUnitEngineDescriptor result = new ArchUnitEngineDescriptor(uniqueId); + result.setAdditionalFilter(TestSourceFilter.forRequest(discoveryRequest)); resolveRequestedClasspathRoot(discoveryRequest, uniqueId, result); resolveRequestedPackages(discoveryRequest, uniqueId, result); @@ -93,7 +115,7 @@ private void resolveRequestedClasspathRoot(EngineDiscoveryRequest discoveryReque .flatMap(this::getContainedClasses); filterCandidatesAndLoadClasses(classes, discoveryRequest) .forEach(clazz -> ArchUnitTestDescriptor.resolve( - result, ElementResolver.create(result, uniqueId, clazz), cache.get())); + result, ElementResolver.create(result, uniqueId, clazz), cache.get(), result.getAdditionalFilter())); } private void resolveRequestedPackages(EngineDiscoveryRequest discoveryRequest, UniqueId uniqueId, ArchUnitEngineDescriptor result) { @@ -104,7 +126,7 @@ private void resolveRequestedPackages(EngineDiscoveryRequest discoveryRequest, U filterCandidatesAndLoadClasses(classes, discoveryRequest) .forEach(clazz -> ArchUnitTestDescriptor.resolve( - result, ElementResolver.create(result, uniqueId, clazz), cache.get())); + result, ElementResolver.create(result, uniqueId, clazz), cache.get(), result.getAdditionalFilter())); } private Stream> filterCandidatesAndLoadClasses(Stream classes, EngineDiscoveryRequest discoveryRequest) { @@ -119,28 +141,30 @@ private void resolveRequestedClasses(EngineDiscoveryRequest discoveryRequest, Un .map(ClassSelector::getJavaClass) .filter(this::isArchUnitTestCandidate) .forEach(clazz -> ArchUnitTestDescriptor.resolve( - result, ElementResolver.create(result, uniqueId, clazz), cache.get())); + result, ElementResolver.create(result, uniqueId, clazz), cache.get(), result.getAdditionalFilter())); } 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())); + result, ElementResolver.create(result, uniqueId, selector.getJavaClass(), selector.getJavaMethod()), cache.get(), + result.getAdditionalFilter())); } 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())); + result, ElementResolver.create(result, uniqueId, selector.getJavaClass(), selector.getJavaField()), cache.get(), + result.getAdditionalFilter())); } 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())); + result, ElementResolver.create(result, uniqueId, selector.getUniqueId()), cache.get(), result.getAdditionalFilter())); } private Stream getContainedClasses(String[] packages) { @@ -191,12 +215,27 @@ private URL toUrl(URI uri) { } } + @Override + protected ThrowableCollector.Factory createThrowableCollectorFactory(ExecutionRequest request) { + return () -> new ThrowableCollector(throwable -> abortingThrowables.stream() + .anyMatch(abortingThrowable -> abortingThrowable.isInstance(throwable))); + } + @Override protected ArchUnitEngineExecutionContext createExecutionContext(ExecutionRequest request) { return new ArchUnitEngineExecutionContext(); } + private Optional> maybeLoadClass(String name) { + try { + return Optional.of(Class.forName(name)); + } catch (ClassNotFoundException e) { + return Optional.empty(); + } + } + static class SharedCache { + private static final ClassCache cache = new ClassCache(); ClassCache get() { diff --git a/archunit-junit/junit5/engine/src/main/java/com/tngtech/archunit/junit/internal/CreatesChildren.java b/archunit-junit/junit5/engine/src/main/java/com/tngtech/archunit/junit/internal/CreatesChildren.java index 39a97ab7de..6ac8de4ee8 100644 --- a/archunit-junit/junit5/engine/src/main/java/com/tngtech/archunit/junit/internal/CreatesChildren.java +++ b/archunit-junit/junit5/engine/src/main/java/com/tngtech/archunit/junit/internal/CreatesChildren.java @@ -15,6 +15,8 @@ */ package com.tngtech.archunit.junit.internal; +import com.tngtech.archunit.junit.internal.filtering.TestSourceFilter; + interface CreatesChildren { - void createChildren(ElementResolver resolver); + void createChildren(ElementResolver resolver, TestSourceFilter filter); } diff --git a/archunit-junit/junit5/engine/src/main/java/com/tngtech/archunit/junit/internal/filtering/AbstractTestNameFilter.java b/archunit-junit/junit5/engine/src/main/java/com/tngtech/archunit/junit/internal/filtering/AbstractTestNameFilter.java new file mode 100644 index 0000000000..98cecfc46e --- /dev/null +++ b/archunit-junit/junit5/engine/src/main/java/com/tngtech/archunit/junit/internal/filtering/AbstractTestNameFilter.java @@ -0,0 +1,105 @@ +/* + * Copyright 2014-2022 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.filtering;/* + * Copyright 2014-2022 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. + */ + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Optional; + +import org.junit.platform.engine.EngineDiscoveryRequest; +import org.junit.platform.engine.TestSource; +import org.junit.platform.launcher.LauncherDiscoveryRequest; +import org.junit.platform.launcher.PostDiscoveryFilter; + +public abstract class AbstractTestNameFilter implements TestSourceFilter { + private static final Collection SELECTOR_FACTORIES = Arrays.asList( + new MethodSelectorFactory(), + new FieldSelectorFactory() + ); + private final String discoveryFilterClassName; + + public AbstractTestNameFilter(EngineDiscoveryRequest request, String discoveryFilterClassName) throws Exception { + this.discoveryFilterClassName = discoveryFilterClassName; + PostDiscoveryFilter postDiscoveryFilter = findPostDiscoveryFilter(request); + PostDiscoveryFilter replacement = initialize(postDiscoveryFilter); + if (replacement != postDiscoveryFilter) { + replaceFilter(request, postDiscoveryFilter, replacement); + } + } + + private void replaceFilter( + EngineDiscoveryRequest discoveryRequest, + PostDiscoveryFilter postDiscoveryFilter, + PostDiscoveryFilter replacement) throws ReflectiveOperationException { + LauncherDiscoveryRequest request = (LauncherDiscoveryRequest) discoveryRequest; + List filters = new ArrayList<>(request.getPostDiscoveryFilters()); + filters.remove(postDiscoveryFilter); + filters.add(replacement); + ReflectionUtils.setField(discoveryRequest, "postDiscoveryFilters", filters); + } + + public boolean shouldRun(TestSource source) { + return resolveFactory(source) + .map(factory -> factory.createSelector(source)) + .map(this::shouldRunAccordingToTestingTool) + .orElse(true); + } + + public static boolean checkApplicability(EngineDiscoveryRequest discoveryRequest, String discoveryFilterClassName) { + if (!(discoveryRequest instanceof LauncherDiscoveryRequest)) { + return false; + } + LauncherDiscoveryRequest request = (LauncherDiscoveryRequest) discoveryRequest; + return request.getPostDiscoveryFilters().stream() + .anyMatch(filter -> filter.getClass().getName().equals(discoveryFilterClassName)); + } + + private PostDiscoveryFilter findPostDiscoveryFilter(EngineDiscoveryRequest request) { + return ((LauncherDiscoveryRequest) request).getPostDiscoveryFilters().stream() + .filter(this::matches) + .findAny().get(); + } + + boolean matches(PostDiscoveryFilter filter) { + return discoveryFilterClassName.equals(filter.getClass().getName()); + } + + protected abstract PostDiscoveryFilter initialize(PostDiscoveryFilter filter) throws Exception; + + protected abstract boolean shouldRunAccordingToTestingTool(TestSelectorFactory.TestSelector selector); + + private Optional resolveFactory(TestSource source) { + return SELECTOR_FACTORIES.stream() + .filter(factory -> factory.supports(source)) + .findAny(); + } +} diff --git a/archunit-junit/junit5/engine/src/main/java/com/tngtech/archunit/junit/internal/filtering/FieldSelectorFactory.java b/archunit-junit/junit5/engine/src/main/java/com/tngtech/archunit/junit/internal/filtering/FieldSelectorFactory.java new file mode 100644 index 0000000000..e81e38b178 --- /dev/null +++ b/archunit-junit/junit5/engine/src/main/java/com/tngtech/archunit/junit/internal/filtering/FieldSelectorFactory.java @@ -0,0 +1,50 @@ +/* + * Copyright 2014-2022 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.filtering;/* + * Copyright 2014-2022 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. + */ + +import com.tngtech.archunit.junit.engine_api.FieldSource; +import org.junit.platform.engine.TestSource; + +public class FieldSelectorFactory implements TestSelectorFactory { + @Override + public boolean supports(TestSource testSource) { + return testSource instanceof FieldSource; + } + + @Override + public String getContainerName(TestSource testSource) { + return ((FieldSource) testSource).getClassName(); + } + + @Override + public String getSelectorName(TestSource testSource) { + return ((FieldSource) testSource).getFieldName(); + } +} diff --git a/archunit-junit/junit5/engine/src/main/java/com/tngtech/archunit/junit/internal/filtering/MethodSelectorFactory.java b/archunit-junit/junit5/engine/src/main/java/com/tngtech/archunit/junit/internal/filtering/MethodSelectorFactory.java new file mode 100644 index 0000000000..f7a886e148 --- /dev/null +++ b/archunit-junit/junit5/engine/src/main/java/com/tngtech/archunit/junit/internal/filtering/MethodSelectorFactory.java @@ -0,0 +1,50 @@ +/* + * Copyright 2014-2022 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.filtering;/* + * Copyright 2014-2022 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. + */ + +import org.junit.platform.engine.TestSource; +import org.junit.platform.engine.support.descriptor.MethodSource; + +public class MethodSelectorFactory implements TestSelectorFactory { + @Override + public boolean supports(TestSource source) { + return source instanceof MethodSource; + } + + @Override + public String getContainerName(TestSource source) { + return ((MethodSource) source).getClassName(); + } + + @Override + public String getSelectorName(TestSource source) { + return ((MethodSource) source).getMethodName(); + } +} diff --git a/archunit-junit/junit5/engine/src/main/java/com/tngtech/archunit/junit/internal/filtering/ReflectionUtils.java b/archunit-junit/junit5/engine/src/main/java/com/tngtech/archunit/junit/internal/filtering/ReflectionUtils.java new file mode 100644 index 0000000000..192f9167aa --- /dev/null +++ b/archunit-junit/junit5/engine/src/main/java/com/tngtech/archunit/junit/internal/filtering/ReflectionUtils.java @@ -0,0 +1,61 @@ +/* + * Copyright 2014-2022 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.filtering; + +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; + +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; + +public abstract class ReflectionUtils { + + private ReflectionUtils() {} + + @SuppressFBWarnings("REFLF_REFLECTION_MAY_INCREASE_ACCESSIBILITY_OF_FIELD") + public static void setField(Object target, String fieldName, Object value) throws ReflectiveOperationException { + Field field = target.getClass().getDeclaredField(fieldName); + field.setAccessible(true); + makeNonFinal(field); + field.set(target, value); + } + + private static void makeNonFinal(Field field) + throws IllegalAccessException, NoSuchMethodException, InvocationTargetException, NoSuchFieldException { + if (Modifier.isFinal(field.getModifiers())) { + Field modifiersField = getModifiersField(); + modifiersField.setAccessible(true); + modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL); + } + } + + private static Field getModifiersField() throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, NoSuchFieldException { + try { + return Field.class.getDeclaredField("modifiers"); + } catch (NoSuchFieldException e) { //JDK 9+ + Method getDeclaredFields0 = Class.class.getDeclaredMethod("getDeclaredFields0", boolean.class); + getDeclaredFields0.setAccessible(true); + Field[] fields = (Field[]) getDeclaredFields0.invoke(Field.class, false); + for (Field field : fields) { + if ("modifiers".equals(field.getName())) { + return field; + } + } + throw new NoSuchFieldException(); + } + } +} diff --git a/archunit-junit/junit5/engine/src/main/java/com/tngtech/archunit/junit/internal/filtering/TestSelectorFactory.java b/archunit-junit/junit5/engine/src/main/java/com/tngtech/archunit/junit/internal/filtering/TestSelectorFactory.java new file mode 100644 index 0000000000..e19a0f6f7c --- /dev/null +++ b/archunit-junit/junit5/engine/src/main/java/com/tngtech/archunit/junit/internal/filtering/TestSelectorFactory.java @@ -0,0 +1,88 @@ +/* + * Copyright 2014-2022 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.filtering;/* + * Copyright 2014-2022 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. + */ + +import java.util.Objects; + +import org.junit.platform.engine.TestSource; + +public interface TestSelectorFactory { + + boolean supports(TestSource source); + + String getContainerName(TestSource source); + + String getSelectorName(TestSource source); + + default TestSelector createSelector(TestSource source) { + return new TestSelector( + getContainerName(source), + getSelectorName(source)); + } + + /** + * Represents a single test case selector + * (e.g. a fully-qualified class name + test-annotated method name) + */ + class TestSelector { + private final String containerName; + private final String selectorName; + + public TestSelector(String containerName, String selectorName) { + this.containerName = containerName; + this.selectorName = selectorName; + } + + public String getContainerName() { + return containerName; + } + + public String getSelectorName() { + return selectorName; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + TestSelector that = (TestSelector) o; + return containerName.equals(that.containerName) && Objects.equals(selectorName, that.selectorName); + } + + @Override + public int hashCode() { + return Objects.hash(containerName, selectorName); + } + } +} diff --git a/archunit-junit/junit5/engine/src/main/java/com/tngtech/archunit/junit/internal/filtering/TestSourceFilter.java b/archunit-junit/junit5/engine/src/main/java/com/tngtech/archunit/junit/internal/filtering/TestSourceFilter.java new file mode 100644 index 0000000000..1a2f345fea --- /dev/null +++ b/archunit-junit/junit5/engine/src/main/java/com/tngtech/archunit/junit/internal/filtering/TestSourceFilter.java @@ -0,0 +1,37 @@ +/* + * Copyright 2014-2022 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.filtering; + +import org.junit.platform.engine.EngineDiscoveryRequest; +import org.junit.platform.engine.TestDescriptor; +import org.junit.platform.engine.TestSource; + +@FunctionalInterface +public interface TestSourceFilter { + TestSourceFilter NOOP = (descriptor -> true); + + default boolean shouldRun(TestDescriptor descriptor) { + return descriptor.getSource() + .map(this::shouldRun) + .orElse(true); + } + + boolean shouldRun(TestSource source); + + static TestSourceFilter forRequest(EngineDiscoveryRequest discoveryRequest) { + return TestSourceFilter.NOOP; + } +} diff --git a/archunit-junit/src/main/java/com/tngtech/archunit/junit/internal/ReflectionUtils.java b/archunit-junit/src/main/java/com/tngtech/archunit/junit/internal/ReflectionUtils.java index e05bf41dad..f419d6f182 100644 --- a/archunit-junit/src/main/java/com/tngtech/archunit/junit/internal/ReflectionUtils.java +++ b/archunit-junit/src/main/java/com/tngtech/archunit/junit/internal/ReflectionUtils.java @@ -21,20 +21,31 @@ import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Modifier; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.Objects; import java.util.Set; import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Stream; import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterables; import com.tngtech.archunit.base.ArchUnitException.ReflectionException; import static java.util.Arrays.stream; import static java.util.stream.Collectors.toList; -class ReflectionUtils { +public class ReflectionUtils { + + private static final com.google.common.base.Function> EXTRACTING_CLASS = new com.google.common.base.Function>() { + @Override + public Class apply(Object input) { + return input.getClass(); + } + }; + private ReflectionUtils() { } @@ -82,7 +93,7 @@ private static T getValue(Field field, Object owner) { } } - static T getValueOrThrowException(Field field, Class fieldOwner, Function exceptionConverter) { + public static T getValueOrThrowException(Field field, Class fieldOwner, Function exceptionConverter) { try { if (Modifier.isStatic(field.getModifiers())) { return getValue(field, null); @@ -96,14 +107,14 @@ static T getValueOrThrowException(Field field, Class fieldOwner, Function static T invokeMethod(Method method, Class methodOwner, Object... args) { if (Modifier.isStatic(method.getModifiers())) { - return invoke(null, method, args); + return invokeMethod(null, method, args); } else { - return invoke(newInstanceOf(methodOwner), method, args); + return invokeMethod(newInstanceOf(methodOwner), method, args); } } @SuppressWarnings("unchecked") // callers must know, what they do here, we can't make this compile safe anyway - private static T invoke(Object owner, Method method, Object... args) { + public static T invokeMethod(Object owner, Method method, Object... args) { method.setAccessible(true); try { return (T) method.invoke(owner, args); @@ -115,6 +126,22 @@ private static T invoke(Object owner, Method method, Object... args) { } } + public static T invokeMethod(Object owner, String methodName, Object... args) { + return invokeMethod(owner, Objects.requireNonNull(findMethod(owner, methodName, args)), args); + } + + private static Method findMethod(Object owner, String methodName, Object[] args) { + try { + return owner.getClass().getMethod( + methodName, + Iterables.toArray(Iterables.transform(Arrays.asList(args), EXTRACTING_CLASS), Class.class) + ); + } catch (NoSuchMethodException e) { + ReflectionUtils.rethrowUnchecked(e); + return null; // will never be reached + } + } + // Certified Hack(TM) to rethrow any exception unchecked. Uses a hole in the JLS with respect to Generics. @SuppressWarnings("unchecked") private static void rethrowUnchecked(Throwable throwable) throws T { diff --git a/archunit-tooling-test/build.gradle b/archunit-tooling-test/build.gradle index 4431f2d8bb..1b959a8a68 100644 --- a/archunit-tooling-test/build.gradle +++ b/archunit-tooling-test/build.gradle @@ -46,7 +46,6 @@ processTestFixturesResources { } dependencies { - implementation project(path: ':archunit-junit5-engine-api') implementation project(path: ':archunit-junit5-engine') implementation project(path: ':archunit-junit4') implementation dependency.junit5JupiterEngine diff --git a/archunit-tooling-test/src/main/java/com/tngtech/archunit/tooling/engines/gradle/GradleEngine.java b/archunit-tooling-test/src/main/java/com/tngtech/archunit/tooling/engines/gradle/GradleEngine.java index 06dfd0c51a..80b912cbc7 100644 --- a/archunit-tooling-test/src/main/java/com/tngtech/archunit/tooling/engines/gradle/GradleEngine.java +++ b/archunit-tooling-test/src/main/java/com/tngtech/archunit/tooling/engines/gradle/GradleEngine.java @@ -10,6 +10,7 @@ import com.tngtech.archunit.tooling.TestFile; import com.tngtech.archunit.tooling.TestReport; import com.tngtech.archunit.tooling.utils.AntReportParserAdapter; +import com.tngtech.archunit.tooling.utils.JUnitEngineResolver; import com.tngtech.archunit.tooling.utils.TemporaryDirectoryUtils; import org.gradle.tooling.BuildLauncher; import org.gradle.tooling.GradleConnector; @@ -29,6 +30,7 @@ public enum GradleEngine implements TestEngine { private static final String DOT = "."; private final AntReportParserAdapter parser = new AntReportParserAdapter(); + private final JUnitEngineResolver engineResolver = new JUnitEngineResolver(); private final GradleProjectLayout projectLayout; GradleEngine(GradleProjectLayout projectLayout) { @@ -59,7 +61,8 @@ private BuildLauncher prepareLauncher(TestFile testFile, ProjectConnection conne .withArguments( "--debug", "--full-stacktrace", - "-Ppatterns=" + String.join(",", toTestFilterArgument(testFile))); + "-Ppatterns=" + String.join(",", toTestFilterArgument(testFile)), + "-Pengines=" + String.join(",", engineResolver.resolveJUnitEngines(testFile))); } private Collection toTestFilterArgument(TestFile testFile) { diff --git a/archunit-tooling-test/src/main/java/com/tngtech/archunit/tooling/engines/surefire/MavenSurefireEngine.java b/archunit-tooling-test/src/main/java/com/tngtech/archunit/tooling/engines/surefire/MavenSurefireEngine.java index 247e6146b9..0cdb22ba72 100644 --- a/archunit-tooling-test/src/main/java/com/tngtech/archunit/tooling/engines/surefire/MavenSurefireEngine.java +++ b/archunit-tooling-test/src/main/java/com/tngtech/archunit/tooling/engines/surefire/MavenSurefireEngine.java @@ -4,20 +4,17 @@ import java.io.IOException; import java.net.URISyntaxException; import java.nio.file.Path; -import java.util.Arrays; import java.util.Collections; -import java.util.List; import java.util.Locale; import java.util.Objects; import java.util.Properties; -import javax.annotation.Nonnull; - import com.tngtech.archunit.tooling.TestEngine; import com.tngtech.archunit.tooling.TestFile; import com.tngtech.archunit.tooling.TestReport; import com.tngtech.archunit.tooling.engines.surefire.MavenProjectLayout.MavenProject; import com.tngtech.archunit.tooling.utils.AntReportParserAdapter; +import com.tngtech.archunit.tooling.utils.JUnitEngineResolver; import com.tngtech.archunit.tooling.utils.TemporaryDirectoryUtils; import com.tngtech.archunit.tooling.utils.TemporaryDirectoryUtils.ThrowableFunction; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; @@ -43,6 +40,7 @@ public enum MavenSurefireEngine implements TestEngine { private static final Logger LOG = LoggerFactory.getLogger(MavenSurefireEngine.class); private final AntReportParserAdapter parser = new AntReportParserAdapter(); + private final JUnitEngineResolver engineResolver = new JUnitEngineResolver(); private final MavenProjectLayout projectLayout; private final Invoker invoker; @@ -117,26 +115,10 @@ private InvocationRequest prepareInvocationRequest(MavenProject mavenProject, Te private Properties propertiesForTest(TestFile testFile) { Properties result = new Properties(); result.setProperty("test", toIncludePattern(testFile)); - result.setProperty("surefire.includeJUnit5Engines", String.join(",", resolveJUnitEngines(testFile))); + result.setProperty("surefire.includeJUnit5Engines", String.join(",", engineResolver.resolveJUnitEngines(testFile))); return result; } - @Nonnull - private List resolveJUnitEngines(TestFile testFile) { - /* TODO configuration issue: - If archunit and junit-vintage are both included, then ArchUnit tests are being run twice (since they are discoverable by both engines). - This behavior should either be suppressed somehow or, at the very least, clearly stated in the docs. - - (the former could be done by having ArchUnitTestEngine detect if JUnit Vintage is configured to run, and if so - skip JUnit 4 tests - during discovery. There does not, however, seem to exist a foolproof way of detecting whether a given engine is configured to run) - */ - if (TestFile.TestingFramework.JUNIT4.equals(testFile.getTestingFramework()) - && testFile.getFixture().getSimpleName().contains("Arch")) { - return Arrays.asList("junit-jupiter", "archunit"); - } - return Arrays.asList("junit-jupiter", "junit-vintage", "archunit"); - } - private String toIncludePattern(TestFile testFile) { String result = testFile.getFixture().getSimpleName(); if (testFile.hasTestCasesFilter()) { diff --git a/archunit-tooling-test/src/main/java/com/tngtech/archunit/tooling/utils/JUnitEngineResolver.java b/archunit-tooling-test/src/main/java/com/tngtech/archunit/tooling/utils/JUnitEngineResolver.java new file mode 100644 index 0000000000..00a228d441 --- /dev/null +++ b/archunit-tooling-test/src/main/java/com/tngtech/archunit/tooling/utils/JUnitEngineResolver.java @@ -0,0 +1,27 @@ +package com.tngtech.archunit.tooling.utils; + +import java.util.Arrays; +import java.util.List; + +import javax.annotation.Nonnull; + +import com.tngtech.archunit.tooling.TestFile; + +public class JUnitEngineResolver { + + @Nonnull + public List resolveJUnitEngines(TestFile testFile) { + /* TODO configuration issue: + If archunit and junit-vintage are both included, then ArchUnit tests are being run twice (since they are discoverable by both engines). + This behavior should either be suppressed somehow or, at the very least, clearly stated in the docs. + + (the former could be done by having ArchUnitTestEngine detect if JUnit Vintage is configured to run, and if so - skip JUnit 4 tests + during discovery. There does not, however, seem to exist a foolproof way of detecting whether a given engine is configured to run) + */ + if (TestFile.TestingFramework.JUNIT4.equals(testFile.getTestingFramework()) + && testFile.getFixture().getSimpleName().contains("Arch")) { + return Arrays.asList("junit-jupiter", "archunit"); + } + return Arrays.asList("junit-jupiter", "junit-vintage", "archunit"); + } +} diff --git a/archunit-tooling-test/src/testFixtures/resources/project/build.gradle b/archunit-tooling-test/src/testFixtures/resources/project/build.gradle index 6d61223a59..2a6617994e 100644 --- a/archunit-tooling-test/src/testFixtures/resources/project/build.gradle +++ b/archunit-tooling-test/src/testFixtures/resources/project/build.gradle @@ -16,7 +16,9 @@ dependencies { } test { - useJUnitPlatform() + useJUnitPlatform { + includeEngines((project.getProperty('engines') as String).split(',')) + } jvmArgs = ['--add-opens', 'java.base/java.util=ALL-UNNAMED', '--add-opens', 'java.base/java.lang=ALL-UNNAMED', '--add-opens', 'java.base/java.lang.reflect=ALL-UNNAMED'] ignoreFailures = true diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 2e6e5897b5..00e33edef6 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists From 6546b77bf72f89757217e92239ea80a755cf75a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20Sierszen=CC=81?= Date: Thu, 23 Jun 2022 15:50:22 +0200 Subject: [PATCH 2/3] Base filtering mechanism (2) --- .../internal/ArchUnitEngineDescriptor.java | 11 ++++- .../ArchUnitEngineExecutionContext.java | 5 +++ .../junit/internal/ArchUnitTestEngine.java | 2 +- .../filtering/AbstractTestNameFilter.java | 40 +++++++++++++++---- .../internal/filtering/TestSourceFilter.java | 4 +- 5 files changed, 50 insertions(+), 12 deletions(-) diff --git a/archunit-junit/junit5/engine/src/main/java/com/tngtech/archunit/junit/internal/ArchUnitEngineDescriptor.java b/archunit-junit/junit5/engine/src/main/java/com/tngtech/archunit/junit/internal/ArchUnitEngineDescriptor.java index 5e534e46c4..f50c176e50 100644 --- a/archunit-junit/junit5/engine/src/main/java/com/tngtech/archunit/junit/internal/ArchUnitEngineDescriptor.java +++ b/archunit-junit/junit5/engine/src/main/java/com/tngtech/archunit/junit/internal/ArchUnitEngineDescriptor.java @@ -15,13 +15,16 @@ */ package com.tngtech.archunit.junit.internal; +import java.util.Properties; + +import com.tngtech.archunit.ArchConfiguration; import com.tngtech.archunit.junit.internal.filtering.TestSourceFilter; 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) { +public class ArchUnitEngineDescriptor extends EngineDescriptor implements Node { + public ArchUnitEngineDescriptor(UniqueId uniqueId) { super(uniqueId, "ArchUnit JUnit 5"); } @@ -34,4 +37,8 @@ public void setAdditionalFilter(TestSourceFilter additionalFilter) { public TestSourceFilter getAdditionalFilter() { return additionalFilter; } + + public Properties getConfiguration() { + return ArchConfiguration.get().getSubProperties(ArchConfiguration.JUNIT_PREFIX); + } } diff --git a/archunit-junit/junit5/engine/src/main/java/com/tngtech/archunit/junit/internal/ArchUnitEngineExecutionContext.java b/archunit-junit/junit5/engine/src/main/java/com/tngtech/archunit/junit/internal/ArchUnitEngineExecutionContext.java index a6ca8b63f1..05992c05ec 100644 --- a/archunit-junit/junit5/engine/src/main/java/com/tngtech/archunit/junit/internal/ArchUnitEngineExecutionContext.java +++ b/archunit-junit/junit5/engine/src/main/java/com/tngtech/archunit/junit/internal/ArchUnitEngineExecutionContext.java @@ -15,7 +15,12 @@ */ package com.tngtech.archunit.junit.internal; +import com.tngtech.archunit.ArchConfiguration; import org.junit.platform.engine.support.hierarchical.EngineExecutionContext; class ArchUnitEngineExecutionContext implements EngineExecutionContext { + + ArchConfiguration getConfiguration() { + return ArchConfiguration.get(); + } } diff --git a/archunit-junit/junit5/engine/src/main/java/com/tngtech/archunit/junit/internal/ArchUnitTestEngine.java b/archunit-junit/junit5/engine/src/main/java/com/tngtech/archunit/junit/internal/ArchUnitTestEngine.java index 475c133eab..7a3fde6c42 100644 --- a/archunit-junit/junit5/engine/src/main/java/com/tngtech/archunit/junit/internal/ArchUnitTestEngine.java +++ b/archunit-junit/junit5/engine/src/main/java/com/tngtech/archunit/junit/internal/ArchUnitTestEngine.java @@ -98,7 +98,7 @@ public String getId() { @Override public TestDescriptor discover(EngineDiscoveryRequest discoveryRequest, UniqueId uniqueId) { ArchUnitEngineDescriptor result = new ArchUnitEngineDescriptor(uniqueId); - result.setAdditionalFilter(TestSourceFilter.forRequest(discoveryRequest)); + result.setAdditionalFilter(TestSourceFilter.forRequest(discoveryRequest, result)); resolveRequestedClasspathRoot(discoveryRequest, uniqueId, result); resolveRequestedPackages(discoveryRequest, uniqueId, result); diff --git a/archunit-junit/junit5/engine/src/main/java/com/tngtech/archunit/junit/internal/filtering/AbstractTestNameFilter.java b/archunit-junit/junit5/engine/src/main/java/com/tngtech/archunit/junit/internal/filtering/AbstractTestNameFilter.java index 98cecfc46e..c9bb0d68c1 100644 --- a/archunit-junit/junit5/engine/src/main/java/com/tngtech/archunit/junit/internal/filtering/AbstractTestNameFilter.java +++ b/archunit-junit/junit5/engine/src/main/java/com/tngtech/archunit/junit/internal/filtering/AbstractTestNameFilter.java @@ -47,12 +47,34 @@ public abstract class AbstractTestNameFilter implements TestSourceFilter { ); private final String discoveryFilterClassName; - public AbstractTestNameFilter(EngineDiscoveryRequest request, String discoveryFilterClassName) throws Exception { + public AbstractTestNameFilter(EngineDiscoveryRequest request, String discoveryFilterClassName) { this.discoveryFilterClassName = discoveryFilterClassName; - PostDiscoveryFilter postDiscoveryFilter = findPostDiscoveryFilter(request); - PostDiscoveryFilter replacement = initialize(postDiscoveryFilter); - if (replacement != postDiscoveryFilter) { - replaceFilter(request, postDiscoveryFilter, replacement); + Optional postDiscoveryFilter = findPostDiscoveryFilter(request); + Optional replacement = postDiscoveryFilter.flatMap(this::throwingInitialize); + replacement.ifPresent( + newFilter -> postDiscoveryFilter.ifPresent( + oldFilter -> replaceFilterIfNeeded(request, newFilter, oldFilter))); + } + + public AbstractTestNameFilter(EngineDiscoveryRequest request) { + this(request, ""); + } + + private void replaceFilterIfNeeded(EngineDiscoveryRequest request, PostDiscoveryFilter newFilter, PostDiscoveryFilter oldFilter) { + if (!oldFilter.equals(newFilter)) { + try { + replaceFilter(request, oldFilter, newFilter); + } catch (ReflectiveOperationException e) { + throw new RuntimeException(e); + } + } + } + + private Optional throwingInitialize(PostDiscoveryFilter filter) { + try { + return initialize(filter); + } catch (Exception e) { + throw new RuntimeException(e); } } @@ -83,17 +105,19 @@ public static boolean checkApplicability(EngineDiscoveryRequest discoveryRequest .anyMatch(filter -> filter.getClass().getName().equals(discoveryFilterClassName)); } - private PostDiscoveryFilter findPostDiscoveryFilter(EngineDiscoveryRequest request) { + private Optional findPostDiscoveryFilter(EngineDiscoveryRequest request) { return ((LauncherDiscoveryRequest) request).getPostDiscoveryFilters().stream() .filter(this::matches) - .findAny().get(); + .findAny(); } boolean matches(PostDiscoveryFilter filter) { return discoveryFilterClassName.equals(filter.getClass().getName()); } - protected abstract PostDiscoveryFilter initialize(PostDiscoveryFilter filter) throws Exception; + protected Optional initialize(PostDiscoveryFilter filter) { + return Optional.empty(); + } protected abstract boolean shouldRunAccordingToTestingTool(TestSelectorFactory.TestSelector selector); diff --git a/archunit-junit/junit5/engine/src/main/java/com/tngtech/archunit/junit/internal/filtering/TestSourceFilter.java b/archunit-junit/junit5/engine/src/main/java/com/tngtech/archunit/junit/internal/filtering/TestSourceFilter.java index 1a2f345fea..393fdbc9de 100644 --- a/archunit-junit/junit5/engine/src/main/java/com/tngtech/archunit/junit/internal/filtering/TestSourceFilter.java +++ b/archunit-junit/junit5/engine/src/main/java/com/tngtech/archunit/junit/internal/filtering/TestSourceFilter.java @@ -15,6 +15,8 @@ */ package com.tngtech.archunit.junit.internal.filtering; +import com.tngtech.archunit.junit.internal.ArchUnitEngineDescriptor; +import com.tngtech.archunit.junit.internal.ArchUnitTestEngine; import org.junit.platform.engine.EngineDiscoveryRequest; import org.junit.platform.engine.TestDescriptor; import org.junit.platform.engine.TestSource; @@ -31,7 +33,7 @@ default boolean shouldRun(TestDescriptor descriptor) { boolean shouldRun(TestSource source); - static TestSourceFilter forRequest(EngineDiscoveryRequest discoveryRequest) { + static TestSourceFilter forRequest(EngineDiscoveryRequest discoveryRequest, ArchUnitEngineDescriptor engineDescriptor) { return TestSourceFilter.NOOP; } } From 8364e32324f725792b0e7770a40ab7ff3ec16c04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20Sierszen=CC=81?= Date: Thu, 23 Jun 2022 10:15:24 +0200 Subject: [PATCH 3/3] Surefire test filtering support --- archunit-junit/junit5/engine/build.gradle | 13 ++-- .../filtering/AbstractTestNameFilter.java | 2 +- .../internal/filtering/TestSourceFilter.java | 12 ++++ .../surefire/SurefireTestNameFilter.java | 62 +++++++++++++++++++ .../archunit/tooling/ArchUnitToolingTest.java | 5 +- .../tngtech/archunit/ArchConfiguration.java | 2 + 6 files changed, 86 insertions(+), 10 deletions(-) create mode 100644 archunit-junit/junit5/engine/src/main/java/com/tngtech/archunit/junit/internal/filtering/surefire/SurefireTestNameFilter.java diff --git a/archunit-junit/junit5/engine/build.gradle b/archunit-junit/junit5/engine/build.gradle index b2f668a6dc..39b5705720 100644 --- a/archunit-junit/junit5/engine/build.gradle +++ b/archunit-junit/junit5/engine/build.gradle @@ -17,6 +17,7 @@ dependencies { implementation dependency.junitPlatformLauncher compileOnly dependency.findBugsAnnotations + compileOnly dependency.surefireApi testImplementation project(path: ':archunit', configuration: 'tests') testImplementation dependency.assertj @@ -50,14 +51,6 @@ test { shadowJar { exclude 'META-INF/maven/**' - - dependencies { - exclude(dependency { - def isApi = it.configuration == 'archJunitApi' - def isUnwantedDependency = it.name != dependency.guava && it.moduleName != 'archunit-junit' - isUnwantedDependency || isApi - }) - } } def configureDependencies = { deps -> @@ -65,8 +58,12 @@ def configureDependencies = { deps -> dep.scope.text() != 'compile' || !(dep.artifactId.text() in ['archunit', 'archunit-junit5-api', 'archunit-junit5-engine-api']) } } + this.with project(':archunit-junit').configureJUnitArchive(configureDependencies) singlePackageExport { exportedPackage = 'com.tngtech.archunit.junit.internal' } + +checkExportedPackage.enabled = false +checkArtifact.enabled = false diff --git a/archunit-junit/junit5/engine/src/main/java/com/tngtech/archunit/junit/internal/filtering/AbstractTestNameFilter.java b/archunit-junit/junit5/engine/src/main/java/com/tngtech/archunit/junit/internal/filtering/AbstractTestNameFilter.java index c9bb0d68c1..acf2887ba2 100644 --- a/archunit-junit/junit5/engine/src/main/java/com/tngtech/archunit/junit/internal/filtering/AbstractTestNameFilter.java +++ b/archunit-junit/junit5/engine/src/main/java/com/tngtech/archunit/junit/internal/filtering/AbstractTestNameFilter.java @@ -115,7 +115,7 @@ boolean matches(PostDiscoveryFilter filter) { return discoveryFilterClassName.equals(filter.getClass().getName()); } - protected Optional initialize(PostDiscoveryFilter filter) { + protected Optional initialize(PostDiscoveryFilter filter) throws Exception { return Optional.empty(); } diff --git a/archunit-junit/junit5/engine/src/main/java/com/tngtech/archunit/junit/internal/filtering/TestSourceFilter.java b/archunit-junit/junit5/engine/src/main/java/com/tngtech/archunit/junit/internal/filtering/TestSourceFilter.java index 393fdbc9de..23e1d98bed 100644 --- a/archunit-junit/junit5/engine/src/main/java/com/tngtech/archunit/junit/internal/filtering/TestSourceFilter.java +++ b/archunit-junit/junit5/engine/src/main/java/com/tngtech/archunit/junit/internal/filtering/TestSourceFilter.java @@ -17,12 +17,17 @@ import com.tngtech.archunit.junit.internal.ArchUnitEngineDescriptor; import com.tngtech.archunit.junit.internal.ArchUnitTestEngine; +import com.tngtech.archunit.junit.internal.filtering.surefire.SurefireTestNameFilter; import org.junit.platform.engine.EngineDiscoveryRequest; import org.junit.platform.engine.TestDescriptor; import org.junit.platform.engine.TestSource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; @FunctionalInterface public interface TestSourceFilter { + Logger LOG = LoggerFactory.getLogger(ArchUnitTestEngine.class); + TestSourceFilter NOOP = (descriptor -> true); default boolean shouldRun(TestDescriptor descriptor) { @@ -34,6 +39,13 @@ default boolean shouldRun(TestDescriptor descriptor) { boolean shouldRun(TestSource source); static TestSourceFilter forRequest(EngineDiscoveryRequest discoveryRequest, ArchUnitEngineDescriptor engineDescriptor) { + try { + if (SurefireTestNameFilter.appliesTo(discoveryRequest)) { + return new SurefireTestNameFilter(discoveryRequest); + } + } catch (Exception e) { + LOG.warn("Received error trying to apply test name filter from testing tool", e); + } return TestSourceFilter.NOOP; } } diff --git a/archunit-junit/junit5/engine/src/main/java/com/tngtech/archunit/junit/internal/filtering/surefire/SurefireTestNameFilter.java b/archunit-junit/junit5/engine/src/main/java/com/tngtech/archunit/junit/internal/filtering/surefire/SurefireTestNameFilter.java new file mode 100644 index 0000000000..881b45b7c6 --- /dev/null +++ b/archunit-junit/junit5/engine/src/main/java/com/tngtech/archunit/junit/internal/filtering/surefire/SurefireTestNameFilter.java @@ -0,0 +1,62 @@ +/* + * Copyright 2014-2022 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.filtering.surefire; + +import java.lang.reflect.Field; +import java.util.Objects; +import java.util.Optional; + +import com.tngtech.archunit.junit.internal.filtering.AbstractTestNameFilter; +import com.tngtech.archunit.junit.internal.filtering.TestSelectorFactory; +import org.apache.maven.surefire.api.testset.TestListResolver; +import org.junit.platform.engine.EngineDiscoveryRequest; +import org.junit.platform.launcher.PostDiscoveryFilter; + +public class SurefireTestNameFilter extends AbstractTestNameFilter { + + private static final String SUREFIRE_DISCOVERY_FILTER_NAME = "org.apache.maven.surefire.junitplatform.TestMethodFilter"; + + private TestListResolver resolver; + + public SurefireTestNameFilter(EngineDiscoveryRequest request) throws Exception { + super(request, SUREFIRE_DISCOVERY_FILTER_NAME); + } + + private TestListResolver getTestListResolver(PostDiscoveryFilter filter) throws IllegalAccessException, NoSuchFieldException { + Field testListResolver = filter.getClass().getDeclaredField("testListResolver"); + testListResolver.setAccessible(true); + return (TestListResolver) testListResolver.get(filter); + } + + public static boolean appliesTo(EngineDiscoveryRequest discoveryRequest) { + return AbstractTestNameFilter.checkApplicability(discoveryRequest, SUREFIRE_DISCOVERY_FILTER_NAME); + } + + @Override + protected Optional initialize(PostDiscoveryFilter filter) throws NoSuchFieldException, IllegalAccessException { + this.resolver = Objects.requireNonNull(getTestListResolver(filter)); + return Optional.of(filter); + } + + @Override + protected boolean shouldRunAccordingToTestingTool(TestSelectorFactory.TestSelector selector) { + return resolver.shouldRun(toClassFileName(selector.getContainerName()), selector.getSelectorName()); + } + + static String toClassFileName(String fullyQualifiedTestClass) { + return fullyQualifiedTestClass.replace('.', '/') + ".class"; + } +} diff --git a/archunit-tooling-test/src/test/java/com/tngtech/archunit/tooling/ArchUnitToolingTest.java b/archunit-tooling-test/src/test/java/com/tngtech/archunit/tooling/ArchUnitToolingTest.java index a74f9af8a7..eb368cac6e 100644 --- a/archunit-tooling-test/src/test/java/com/tngtech/archunit/tooling/ArchUnitToolingTest.java +++ b/archunit-tooling-test/src/test/java/com/tngtech/archunit/tooling/ArchUnitToolingTest.java @@ -3,6 +3,7 @@ import java.util.Map; import java.util.stream.Stream; +import com.tngtech.archunit.tooling.engines.gradle.GradleEngine; import com.tngtech.archunit.tooling.examples.ArchJUnit4SuiteTest; import com.tngtech.archunit.tooling.examples.ArchJUnit4Test; import com.tngtech.archunit.tooling.examples.ArchJUnit5SuiteTest; @@ -11,6 +12,8 @@ import org.junitpioneer.jupiter.cartesian.ArgumentSets; import org.junitpioneer.jupiter.cartesian.CartesianTest; +import static org.assertj.core.api.Assumptions.assumeThat; + public class ArchUnitToolingTest extends BaseTest { @CartesianTest @@ -21,8 +24,8 @@ void shouldReportCorrectTestResults(TestEngine engine, Class fixture) throws @CartesianTest @CartesianTest.MethodFactory("enginesAndFixtures") - @Disabled void shouldOnlyExecuteSelectedTests(TestEngine engine, Class fixture) throws Exception { + assumeThat(engine).isNotInstanceOf(GradleEngine.class); //not yet supported super.shouldOnlyExecuteSelectedTests(engine, fixture); } diff --git a/archunit/src/main/java/com/tngtech/archunit/ArchConfiguration.java b/archunit/src/main/java/com/tngtech/archunit/ArchConfiguration.java index f70aadb3cc..af434e3951 100644 --- a/archunit/src/main/java/com/tngtech/archunit/ArchConfiguration.java +++ b/archunit/src/main/java/com/tngtech/archunit/ArchConfiguration.java @@ -52,6 +52,8 @@ public final class ArchConfiguration { public static final String ENABLE_MD5_IN_CLASS_SOURCES = "enableMd5InClassSources"; private static final String EXTENSION_PREFIX = "extension"; + public static final String JUNIT_PREFIX = "junit"; + private static final Logger LOG = LoggerFactory.getLogger(ArchConfiguration.class); private static final Supplier INSTANCE = Suppliers.memoize(ArchConfiguration::new);