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 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))); } }