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.
*
* This is run as part of CI, so it's expected to kept passing at all times.
*
* 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 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 haveJUnitUtilSetupAndTeardown() {
return new SetupTearDownCondition("have JUnitUtil.setUp/tearDown in @BeforeEach/@AfterEach (or super)");
}
private static class SetupTearDownCondition extends ArchCondition {
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 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)));
}
}