322 lines
14 KiB
Java
322 lines
14 KiB
Java
package jmri;
|
|
|
|
import java.lang.annotation.Annotation;
|
|
import java.util.stream.Stream;
|
|
|
|
import org.junit.jupiter.api.*;
|
|
import org.junit.jupiter.params.ParameterizedTest;
|
|
|
|
import org.netbeans.jemmy.QueueTool;
|
|
|
|
import com.tngtech.archunit.base.DescribedPredicate;
|
|
import com.tngtech.archunit.core.domain.JavaClass;
|
|
import com.tngtech.archunit.core.domain.JavaMethod;
|
|
import com.tngtech.archunit.core.domain.JavaModifier;
|
|
import com.tngtech.archunit.lang.*;
|
|
import com.tngtech.archunit.lang.syntax.ArchRuleDefinition;
|
|
import com.tngtech.archunit.junit.*;
|
|
|
|
import static com.tngtech.archunit.lang.conditions.ArchConditions.callMethod;
|
|
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.*;
|
|
|
|
/**
|
|
* Check the architecture of the JMRI library Tests.
|
|
* <p>
|
|
* This is run as part of CI, so it's expected to kept passing at all times.
|
|
* <p>
|
|
* Note that this only checks the classes in target/test-classes, which come from java/test, not
|
|
* the ones in target/classes, which come from java/src. It's relying on the common
|
|
* build procedure to make this distinction.
|
|
* Based on {@link ArchitectureTest}
|
|
*
|
|
* See examples in the <a href='https://github.com/TNG/ArchUnit-Examples/tree/master/example-plain/src/test/java/com/tngtech/archunit/exampletest">ArchUnit sample code</a>.
|
|
*
|
|
* @author Bob Jacobsen 2019
|
|
* @author Steve Young Copyright (c) 2022
|
|
*/
|
|
|
|
// Pick up all classes from the target/test-classes directory, which is the test code
|
|
@AnalyzeClasses(packages = {"target/test-classes"}) // "jmri","apps"
|
|
|
|
public class TestArchitectureTest {
|
|
|
|
// want these statics first in class, to initialize
|
|
// logging before various static items are constructed
|
|
@BeforeAll // tests are static.
|
|
public static void setUp() {
|
|
jmri.util.JUnitUtil.setUp();
|
|
}
|
|
|
|
@AfterAll
|
|
public static void tearDown() {
|
|
jmri.util.JUnitUtil.tearDown();
|
|
}
|
|
|
|
/**
|
|
* Prevent @RepeatedTest annotations from being accidentally merged.
|
|
*/
|
|
@ArchTest
|
|
public static final ArchRule repeatedTestRule = noMethods().should()
|
|
.beAnnotatedWith(org.junit.jupiter.api.RepeatedTest.class);
|
|
|
|
/**
|
|
* Please use org.junit.jupiter.api.Test
|
|
*/
|
|
@ArchTest
|
|
public static final ArchRule junit4TestRule = noClasses()
|
|
.should().dependOnClassesThat().haveFullyQualifiedName("org.junit.Test") // JUnit4
|
|
.because("Tests should normally use org.junit.jupiter.api.Test"); // JUnit5
|
|
|
|
@ArchTest
|
|
public static final ArchRule methodsStartingWithTestShouldBeAnnotatedWithTest =
|
|
ArchRuleDefinition.methods()
|
|
.that(new DescribedPredicate<JavaMethod>("name starts with 'test', missing test annotations") {
|
|
@Override
|
|
public boolean test(JavaMethod method) {
|
|
boolean nameMatches = method.getName().toLowerCase().startsWith("test");
|
|
boolean noParams = method.getRawParameterTypes().isEmpty();
|
|
boolean notAbstract = !method.getModifiers().contains(JavaModifier.ABSTRACT);
|
|
boolean isVoid = method.getRawReturnType().isEquivalentTo(void.class);
|
|
boolean hasAnyTestAnnotation =
|
|
method.isAnnotatedWith( ParameterizedTest.class);
|
|
|
|
// Return true for methods that start with test, have no params, AND are missing the annotations
|
|
return nameMatches && noParams && notAbstract && isVoid && !hasAnyTestAnnotation;
|
|
}
|
|
})
|
|
// Point failures towards org.junit.jupiter.api.Test , not Parameterized / Abstract
|
|
.should().beAnnotatedWith(Test.class);
|
|
|
|
/**
|
|
* Please use org.junit.jupiter.api.BeforeEach
|
|
*/
|
|
@ArchTest
|
|
public static final ArchRule junit4BeforeRule = noClasses()
|
|
.should().dependOnClassesThat().haveFullyQualifiedName("org.junit.Before") // JUnit4
|
|
.because("Tests should normally use org.junit.jupiter.api.BeforeEach"); // JUnit5
|
|
|
|
/**
|
|
* Please use org.junit.jupiter.api.AfterEach
|
|
*/
|
|
@ArchTest
|
|
public static final ArchRule junit4AfterRule = noClasses()
|
|
.should().dependOnClassesThat().haveFullyQualifiedName("org.junit.After") // JUnit4
|
|
.because("Tests should normally use org.junit.jupiter.api.AfterEach"); // JUnit5
|
|
|
|
/**
|
|
* please use org.junit.jupiter.api.Disabled
|
|
*/
|
|
@ArchTest
|
|
public static final ArchRule junit4IgnoreRule = noClasses()
|
|
.should().dependOnClassesThat().haveFullyQualifiedName("org.junit.Ignore") // JUnit4
|
|
.because("To Ignore, tests should use org.junit.jupiter.api.Disabled or jmri.util.junit.annotations.NotApplicable"); // JUnit5
|
|
|
|
/**
|
|
* JMRI should not reference org.apache.log4j to allow JMRI
|
|
* to be used as library in applications that choose not to use Log4J.
|
|
*/
|
|
@ArchTest
|
|
public static final ArchRule noLog4JinJmriTestsRule = noClasses()
|
|
.that().doNotHaveFullyQualifiedName("jmri.util.JUnitAppender")
|
|
.and().doNotHaveFullyQualifiedName("jmri.util.JUnitAppenderTest")
|
|
.and().doNotHaveFullyQualifiedName("jmri.util.TestingLoggerConfiguration")
|
|
.and().doNotHaveFullyQualifiedName("apps.jmrit.log.Log4JTreePaneTest")
|
|
.should().dependOnClassesThat().resideInAPackage("org.apache.logging.log4j")
|
|
.because("Tests should normally use org.slf4j.Logger instead of Log4J");
|
|
|
|
/**
|
|
* JMRI tests should use org.slf4j.Logger instead of JUL.
|
|
*/
|
|
@ArchTest
|
|
public static final ArchRule minimalJULinJmriTests = noClasses()
|
|
.that().resideInAPackage("jmri..")
|
|
.and().doNotHaveFullyQualifiedName("jmri.util.TestingLoggerConfiguration") // tests jul routed to l4j OK
|
|
.and().doNotHaveFullyQualifiedName("jmri.util.web.BrowserFactory") // 3rd party lib setup
|
|
.and().doNotHaveFullyQualifiedName("jmri.web.WebServerAcceptanceSteps") // testing output from lib
|
|
.should().dependOnClassesThat().resideInAPackage("java.util.logging")
|
|
.because("Tests should normally use org.slf4j.Logger instead of JUL");
|
|
|
|
/**
|
|
* setUp methods should normally use the org.junit.jupiter.api.BeforeEach annotation.
|
|
*/
|
|
@ArchTest
|
|
public static final ArchRule setUpMethodsHaveBeforeEachAnnotation =
|
|
methods()
|
|
.that().haveName("setUp")
|
|
.and().doNotHaveModifier(JavaModifier.ABSTRACT)
|
|
.and().areNotDeclaredIn(jmri.util.JUnitUtil.class)
|
|
.should()
|
|
.beAnnotatedWith(BeforeEach.class)
|
|
.orShould().beAnnotatedWith(BeforeAll.class)
|
|
.because("setUp methods should normally use the BeforeEach annotation");
|
|
|
|
/**
|
|
* tearDown methods should normally use the org.junit.jupiter.api.AfterEach annotation.
|
|
*/
|
|
@ArchTest
|
|
public static final ArchRule tearDownMethodsHaveAfterEachAnnotation =
|
|
methods()
|
|
.that().haveName("tearDown")
|
|
.and().doNotHaveModifier(JavaModifier.ABSTRACT)
|
|
.and().areNotDeclaredIn(jmri.util.JUnitUtil.class)
|
|
.should()
|
|
.beAnnotatedWith(AfterEach.class)
|
|
.orShould().beAnnotatedWith(AfterAll.class)
|
|
.because("tearDown methods should normally use the AfterEach annotation");
|
|
|
|
/**
|
|
* JMRI does not require PackageTest.class .
|
|
*/
|
|
@ArchTest
|
|
public static final ArchRule noJUnit4PackageTestsRule = noClasses()
|
|
.should().haveSimpleName("PackageTest")
|
|
.because("JMRI does not require PackageTest.class");
|
|
|
|
/**
|
|
* JUnit5 should not have abstract methods with Test annotation.
|
|
* Instead, the overriding method should have the Test annotation.
|
|
*/
|
|
@ArchTest
|
|
public static final ArchRule noAbstractTestMethods = noMethods()
|
|
.that().areAnnotatedWith(Test.class)
|
|
.or().areAnnotatedWith(org.junit.jupiter.params.ParameterizedTest.class)
|
|
.should()
|
|
.haveModifier(JavaModifier.ABSTRACT)
|
|
.because("The overriding method should have the Test annotation, not the abstract.");
|
|
|
|
/**
|
|
* JUnit5 should not have abstract methods with LifeCycle annotation.
|
|
* Instead, the overriding method should have the annotation.
|
|
*/
|
|
@ArchTest
|
|
public static final ArchRule noAbstractLifeCycleMethods = noMethods()
|
|
.that().areAnnotatedWith(BeforeEach.class)
|
|
.or().areAnnotatedWith(AfterEach.class)
|
|
.or().areAnnotatedWith(BeforeAll.class)
|
|
.or().areAnnotatedWith(AfterAll.class)
|
|
.should()
|
|
.haveModifier(JavaModifier.ABSTRACT)
|
|
.because("The overriding method should have the Test Lifecycle annotation, not the abstract.");
|
|
|
|
/**
|
|
* Tests should not have empty methods
|
|
* as this saves invoking both the setUp and tearDown Class methods.
|
|
*/
|
|
@ArchTest
|
|
public static final ArchRule no_empty_test_methods =
|
|
ArchRuleDefinition.methods()
|
|
.that().areAnnotatedWith(Test.class)
|
|
.and().areNotAnnotatedWith(Disabled.class)
|
|
.and().areNotAnnotatedWith(jmri.util.junit.annotations.NotApplicable.class)
|
|
// java assert may be removed in compilation resulting in an empty method
|
|
.and().areNotDeclaredIn(jmri.util.junit.AssertTest.class)
|
|
.should(notBeEmpty())
|
|
.because("this saves invoking both the setUp and tearDown Class methods. "
|
|
+"Please use @Disabled or @jmri.util.junit.annotations.NotApplicable");
|
|
|
|
private static ArchCondition<JavaMethod> notBeEmpty() {
|
|
return new ArchCondition<>("not be empty") {
|
|
@Override
|
|
public void check(JavaMethod method, ConditionEvents events) {
|
|
if (method.getMethodCallsFromSelf().isEmpty() && method.getRawParameterTypes().isEmpty()) {
|
|
String message = String.format("Method %s is empty", method.getFullName());
|
|
events.add(SimpleConditionEvent.violated(method, message));
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Tests should use JUnit assertions, not native Java asserts.
|
|
* Note that although we cannot directly test for usage of the assert keyword,
|
|
* we can check for classes that use java.lang.AssertionError.class
|
|
*/
|
|
@ArchTest
|
|
public static final ArchRule noNativeAsserts = noClasses()
|
|
.that().doNotHaveFullyQualifiedName("jmri.util.junit.AssertTest")
|
|
.should().accessClassesThat()
|
|
.haveFullyQualifiedName(AssertionError.class.getName())
|
|
.because("Please use JUnit Assertions rather than the Java 'assert' keyword");
|
|
|
|
@ArchTest
|
|
public static final ArchRule noQueueToolTimedWaitEmpty = noClasses()
|
|
.that().doNotHaveFullyQualifiedName("jmri.jmrit.operations.OperationsTestCase") // used in non-CI tearDown testing
|
|
.should( callMethod(QueueTool.class, "waitEmpty", long.class))
|
|
.because("Due to timers or cursor flashes, the queue may never become empty for a duration. "
|
|
+ "Please call new QueueTool().waitEmpty() without a timeout, or use JUnitUtil.waitFor(xx)");
|
|
|
|
@ArchTest
|
|
public static final ArchRule tests_should_use_JUnitUtil = classes()
|
|
.that().areNotInterfaces()
|
|
.and().doNotHaveModifier(JavaModifier.ABSTRACT ) // exclude abstract classes
|
|
.should(haveJUnitUtilSetupAndTeardown());
|
|
|
|
private static ArchCondition<JavaClass> haveJUnitUtilSetupAndTeardown() {
|
|
return new SetupTearDownCondition("have JUnitUtil.setUp/tearDown in @BeforeEach/@AfterEach (or super)");
|
|
}
|
|
|
|
private static class SetupTearDownCondition extends ArchCondition<JavaClass> {
|
|
|
|
SetupTearDownCondition(String description) {
|
|
super(description);
|
|
}
|
|
|
|
@Override
|
|
public void check(JavaClass javaClass, ConditionEvents events) {
|
|
if (!isJUnitTestClass(javaClass) || isBundleTest(javaClass) ) {
|
|
return;
|
|
}
|
|
|
|
boolean hasSetup =
|
|
hasAnnotatedMethodCallingJUnitUtil(javaClass, BeforeEach.class, "setUp")
|
|
|| hasAnnotatedMethodCallingJUnitUtil(javaClass, BeforeAll.class, "setUp")
|
|
|| hasAnnotatedMethodCallingJUnitUtil(javaClass, BeforeEach.class, "setUpLoggingAndCommonProperties")
|
|
|| hasAnnotatedMethodCallingJUnitUtil(javaClass, BeforeAll.class, "setUpLoggingAndCommonProperties");
|
|
|
|
boolean hasTeardown =
|
|
hasAnnotatedMethodCallingJUnitUtil(javaClass, AfterEach.class, "tearDown")
|
|
|| hasAnnotatedMethodCallingJUnitUtil(javaClass, AfterAll.class, "tearDown");
|
|
|
|
if (!hasSetup) {
|
|
events.add(SimpleConditionEvent.violated( javaClass,
|
|
javaClass.getName() + " has tests but no @BeforeEach or super calling JUnitUtil.setUp()"
|
|
));
|
|
}
|
|
if (!hasTeardown) {
|
|
events.add(SimpleConditionEvent.violated( javaClass,
|
|
javaClass.getName() + " has tests but no @AfterEach or super calling JUnitUtil.tearDown()"
|
|
));
|
|
}
|
|
}
|
|
}
|
|
|
|
private static boolean isJUnitTestClass(JavaClass c) {
|
|
return c.getMethods().stream().anyMatch(m ->
|
|
m.isAnnotatedWith( Test.class)
|
|
|| m.isAnnotatedWith(ArchTest.class)
|
|
|| m.isAnnotatedWith( ParameterizedTest.class));
|
|
}
|
|
|
|
private static boolean isBundleTest(JavaClass c) {
|
|
return c.getSimpleName().endsWith("BundleTest");
|
|
}
|
|
|
|
private static final String JUNIT_UTIL = "jmri.util.JUnitUtil";
|
|
|
|
private static boolean hasAnnotatedMethodCallingJUnitUtil( JavaClass clazz,
|
|
Class<? extends Annotation> annotationType, String junitUtilMethodName) {
|
|
|
|
// Build a stream of this class plus all its raw superclasses
|
|
Stream<JavaClass> classesToCheck =
|
|
Stream.concat(Stream.of(clazz), clazz.getAllRawSuperclasses().stream());
|
|
|
|
return classesToCheck
|
|
.flatMap(c -> c.getMethods().stream())
|
|
.filter(m -> m.isAnnotatedWith(annotationType))
|
|
.anyMatch(m -> m.getMethodCallsFromSelf().stream().anyMatch(call ->
|
|
call.getTargetOwner().getName().equals(JUNIT_UTIL)
|
|
&& call.getName().equals(junitUtilMethodName)));
|
|
}
|
|
|
|
}
|