Skip to content

Commit

Permalink
move junit implementation details to subpackage internal
Browse files Browse the repository at this point in the history
Since we use the `MANIFEST` entry `Automatic-Module-Name` the JUnit 5 modules are principally ready to be used on the modulepath. However, we automatically expose **all** packages declared, which means that the modules `junit5-api` and `junit5-engine` all export the same package `com.tngtech.archunit.junit`, which is illegal for Java Modules. We move all implementation details to a subpackage `internal` and only keep API toplevel. This way the module `junit5-engine` does not expose the root package `com.tngtech.archunit.junit` anymore, because all the contained classes are `internal`. While this separation works nicely and cleanly for `junit5`, the separation for `junit4` is a lot less pretty. Because the architecture of JUnit 4 makes it impossible to hide implementation details in a clean way, e.g. by demanding to annotate a `Runner` implementation, which should be pure hidden implementation detail. I added some separation by introducing `ArchUnitRunnerInternal` so we do not need to make all classes public, but have one (obviously not mean for outside usage) public gateway into the `internal` part.

Signed-off-by: Peter Gafert <peter.gafert@tngtech.com>
  • Loading branch information
codecholeric committed Apr 20, 2022
1 parent 6625f3f commit ba452bf
Show file tree
Hide file tree
Showing 69 changed files with 564 additions and 417 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,32 +15,20 @@
*/
package com.tngtech.archunit.junit;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Set;

import com.google.common.collect.ImmutableSet;
import com.tngtech.archunit.Internal;
import com.tngtech.archunit.PublicAPI;
import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.importer.ImportOption;
import com.tngtech.archunit.base.MayResolveTypesViaReflection;
import com.tngtech.archunit.base.ReflectionUtils;
import com.tngtech.archunit.lang.ArchRule;
import org.junit.runner.Description;
import org.junit.runner.notification.RunNotifier;
import org.junit.runners.ParentRunner;
import org.junit.runners.model.FrameworkField;
import org.junit.runners.model.FrameworkMethod;
import org.junit.runners.model.InitializationError;
import org.junit.runners.model.Statement;

import static com.tngtech.archunit.PublicAPI.Usage.ACCESS;
import static com.tngtech.archunit.junit.ArchRuleDeclaration.elementShouldBeIgnored;
import static com.tngtech.archunit.junit.ArchRuleDeclaration.toDeclarations;
import static com.tngtech.archunit.junit.ArchTestExecution.getValue;

/**
* Evaluates {@link ArchRule ArchRules} against the classes inside of the packages specified via
Expand All @@ -58,169 +46,61 @@
* }
* </code></pre>
*
* The runner will cache classes between test runs, for details please refer to {@link ClassCache}.
* The runner will cache classes between test runs, for details please refer to {@link com.tngtech.archunit.junit.internal.ClassCache}.
*/
@PublicAPI(usage = ACCESS)
public class ArchUnitRunner extends ParentRunner<ArchTestExecution> {
private SharedCache cache = new SharedCache(); // NOTE: We want to change this in tests -> no static/final reference
public class ArchUnitRunner<T> extends ParentRunner<T> {
private final InternalRunner<T> runnerDelegate;

@Internal
public ArchUnitRunner(Class<?> testClass) throws InitializationError {
super(testClass);
checkAnnotation(testClass);
runnerDelegate = createInternalRunner(testClass);
}

private static AnalyzeClasses checkAnnotation(Class<?> testClass) {
AnalyzeClasses analyzeClasses = testClass.getAnnotation(AnalyzeClasses.class);
ArchTestInitializationException.check(analyzeClasses != null,
"Class %s must be annotated with @%s",
testClass.getSimpleName(), AnalyzeClasses.class.getSimpleName());
return analyzeClasses;
private InternalRunner<T> createInternalRunner(Class<?> testClass) {
Class<InternalRunner<T>> runnerClass = classForName("com.tngtech.archunit.junit.internal.ArchUnitRunnerInternal");
return ReflectionUtils.newInstanceOf(runnerClass, testClass);
}

@Override
protected Statement classBlock(RunNotifier notifier) {
final Statement statement = super.classBlock(notifier);
return new Statement() {
@Override
public void evaluate() throws Throwable {
try {
statement.evaluate();
} finally {
cache.clear(getTestClass().getJavaClass());
}
}
};
}

@Override
protected List<ArchTestExecution> getChildren() {
List<ArchTestExecution> children = new ArrayList<>();
children.addAll(findArchRuleFields());
children.addAll(findArchRuleMethods());
return children;
}

private Collection<ArchTestExecution> findArchRuleFields() {
List<ArchTestExecution> result = new ArrayList<>();
for (FrameworkField ruleField : getTestClass().getAnnotatedFields(ArchTest.class)) {
result.addAll(findArchRulesIn(ruleField));
}
return result;
}

private Set<ArchTestExecution> findArchRulesIn(FrameworkField ruleField) {
boolean ignore = elementShouldBeIgnored(ruleField.getField());
if (ruleField.getType() == ArchTests.class) {
return asTestExecutions(getArchRules(ruleField.getField()), ignore);
}
return Collections.<ArchTestExecution>singleton(new ArchRuleExecution(getTestClass().getJavaClass(), ruleField.getField(), ignore));
}

private Set<ArchTestExecution> asTestExecutions(ArchTests archTests, boolean forceIgnore) {
ExecutionTransformer executionTransformer = new ExecutionTransformer();
for (ArchRuleDeclaration<?> declaration : toDeclarations(archTests, getTestClass().getJavaClass(), ArchTest.class, forceIgnore)) {
declaration.handleWith(executionTransformer);
@SuppressWarnings("unchecked")
@MayResolveTypesViaReflection(reason = "Only used to load internal ArchUnit class")
private static <T> Class<T> classForName(String typeName) {
try {
return (Class<T>) Class.forName(typeName);
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
return executionTransformer.getExecutions();
}

private ArchTests getArchRules(Field field) {
return getValue(field, field.getDeclaringClass());
}

private Collection<ArchTestExecution> findArchRuleMethods() {
List<ArchTestExecution> result = new ArrayList<>();
for (FrameworkMethod testMethod : getTestClass().getAnnotatedMethods(ArchTest.class)) {
boolean ignore = elementShouldBeIgnored(testMethod.getMethod());
result.add(new ArchTestMethodExecution(getTestClass().getJavaClass(), testMethod.getMethod(), ignore));
}
return result;
}

@Override
protected Description describeChild(ArchTestExecution child) {
return child.describeSelf();
protected Statement classBlock(RunNotifier notifier) {
return runnerDelegate.classBlock(notifier);
}

@Override
protected void runChild(ArchTestExecution child, RunNotifier notifier) {
if (child.ignore()) {
notifier.fireTestIgnored(describeChild(child));
} else {
notifier.fireTestStarted(describeChild(child));
Class<?> testClass = getTestClass().getJavaClass();
JavaClasses classes = cache.get().getClassesToAnalyzeFor(testClass, new JUnit4ClassAnalysisRequest(testClass));
child.evaluateOn(classes).notify(notifier);
notifier.fireTestFinished(describeChild(child));
}
protected List<T> getChildren() {
return runnerDelegate.getChildren();
}

static class SharedCache {
private static final ClassCache cache = new ClassCache();

ClassCache get() {
return cache;
}

void clear(Class<?> testClass) {
cache.clear(testClass);
}
@Override
protected Description describeChild(T child) {
return runnerDelegate.describeChild(child);
}

private static class ExecutionTransformer implements ArchRuleDeclaration.Handler {
private final ImmutableSet.Builder<ArchTestExecution> executions = ImmutableSet.builder();

@Override
public void handleFieldDeclaration(Field field, Class<?> fieldOwner, boolean ignore) {
executions.add(new ArchRuleExecution(fieldOwner, field, ignore));
}

@Override
public void handleMethodDeclaration(Method method, Class<?> methodOwner, boolean ignore) {
executions.add(new ArchTestMethodExecution(methodOwner, method, ignore));
}

Set<ArchTestExecution> getExecutions() {
return executions.build();
}
@Override
protected void runChild(T child, RunNotifier notifier) {
runnerDelegate.runChild(child, notifier);
}

private static class JUnit4ClassAnalysisRequest implements ClassAnalysisRequest {
private final AnalyzeClasses analyzeClasses;

JUnit4ClassAnalysisRequest(Class<?> testClass) {
analyzeClasses = checkAnnotation(testClass);
}

@Override
public String[] getPackageNames() {
return analyzeClasses.packages();
}

@Override
public Class<?>[] getPackageRoots() {
return analyzeClasses.packagesOf();
}

@Override
public Class<? extends LocationProvider>[] getLocationProviders() {
return analyzeClasses.locations();
}
@Internal
public interface InternalRunner<T> {
Statement classBlock(RunNotifier notifier);

@Override
public Class<? extends ImportOption>[] getImportOptions() {
return analyzeClasses.importOptions();
}
List<T> getChildren();

@Override
public CacheMode getCacheMode() {
return analyzeClasses.cacheMode();
}
Description describeChild(T child);

@Override
public boolean scanWholeClasspath() {
return analyzeClasses.wholeClasspath();
}
void runChild(T child, RunNotifier notifier);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.tngtech.archunit.junit;
package com.tngtech.archunit.junit.internal;

import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
Expand All @@ -24,12 +24,14 @@
import java.util.Set;

import com.google.common.collect.ImmutableSet;
import com.tngtech.archunit.junit.ArchIgnore;
import com.tngtech.archunit.junit.ArchTests;

import static com.google.common.base.Preconditions.checkNotNull;
import static com.tngtech.archunit.junit.ArchTestExecution.getValue;
import static com.tngtech.archunit.junit.ReflectionUtils.getAllFields;
import static com.tngtech.archunit.junit.ReflectionUtils.getAllMethods;
import static com.tngtech.archunit.junit.ReflectionUtils.withAnnotation;
import static com.tngtech.archunit.junit.internal.ArchTestExecution.getValue;
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;

abstract class ArchRuleDeclaration<T extends AnnotatedElement> {
private final Class<?> testClass;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,15 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.tngtech.archunit.junit;
package com.tngtech.archunit.junit.internal;

import java.lang.reflect.Field;

import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.lang.ArchRule;
import org.junit.runner.Description;

import static com.tngtech.archunit.junit.DisplayNameResolver.determineDisplayName;
import static com.tngtech.archunit.junit.internal.DisplayNameResolver.determineDisplayName;

class ArchRuleExecution extends ArchTestExecution {
private final Field ruleField;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.tngtech.archunit.junit;
package com.tngtech.archunit.junit.internal;

import java.lang.reflect.Field;

Expand All @@ -22,8 +22,8 @@
import org.junit.runner.notification.Failure;
import org.junit.runner.notification.RunNotifier;

import static com.tngtech.archunit.junit.ArchTestInitializationException.WRAP_CAUSE;
import static com.tngtech.archunit.junit.ReflectionUtils.getValueOrThrowException;
import static com.tngtech.archunit.junit.internal.ArchTestInitializationException.WRAP_CAUSE;
import static com.tngtech.archunit.junit.internal.ReflectionUtils.getValueOrThrowException;

abstract class ArchTestExecution {
final Class<?> testClass;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,17 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.tngtech.archunit.junit;
package com.tngtech.archunit.junit.internal;

import java.lang.reflect.Method;
import java.util.Arrays;

import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.junit.ArchTest;
import org.junit.runner.Description;

import static com.tngtech.archunit.junit.DisplayNameResolver.determineDisplayName;
import static com.tngtech.archunit.junit.ReflectionUtils.invokeMethod;
import static com.tngtech.archunit.junit.internal.DisplayNameResolver.determineDisplayName;
import static com.tngtech.archunit.junit.internal.ReflectionUtils.invokeMethod;

class ArchTestMethodExecution extends ArchTestExecution {
private final Method testMethod;
Expand Down
Loading

0 comments on commit ba452bf

Please sign in to comment.