test/jdk/tools/jpackage/helpers/jdk/jpackage/test/TestBuilder.java
branchJDK-8200758-branch
changeset 58648 3bf53ffa9ae7
parent 58416 f09bf58c1f17
--- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/TestBuilder.java	Wed Oct 16 09:57:23 2019 -0400
+++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/TestBuilder.java	Wed Oct 16 10:32:08 2019 -0400
@@ -23,43 +23,66 @@
 
 package jdk.jpackage.test;
 
-import java.lang.reflect.Constructor;
+import java.lang.reflect.Array;
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
+import java.lang.reflect.Modifier;
+import java.util.*;
+import java.util.concurrent.atomic.AtomicInteger;
 import java.util.function.Consumer;
 import java.util.function.Function;
+import java.util.function.UnaryOperator;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
+import jdk.jpackage.test.Annotations.AfterEach;
+import jdk.jpackage.test.Annotations.BeforeEach;
 import jdk.jpackage.test.Annotations.Parameter;
+import jdk.jpackage.test.Annotations.ParameterGroup;
 import jdk.jpackage.test.Annotations.Parameters;
 import jdk.jpackage.test.Annotations.Test;
 import jdk.jpackage.test.Functional.ThrowingConsumer;
 import jdk.jpackage.test.Functional.ThrowingFunction;
-import jdk.jpackage.test.Functional.ThrowingSupplier;
 
 final class TestBuilder implements AutoCloseable {
 
     @Override
     public void close() throws Exception {
-        flushTestGroup(null);
+        flushTestGroup();
     }
 
     TestBuilder(Consumer<TestInstance> testConsumer) {
-        argProcessors = Map.of(CMDLINE_ARG_PREFIX + "after-run",
-                arg -> getJavaMethodsFromArg(arg).forEach(
-                        (method) -> afterActions.add(wrap(method, dryRun))),
+        argProcessors = Map.of(
+                CMDLINE_ARG_PREFIX + "after-run",
+                arg -> getJavaMethodsFromArg(arg).map(
+                        this::wrap).forEachOrdered(afterActions::add),
+
                 CMDLINE_ARG_PREFIX + "before-run",
-                arg -> getJavaMethodsFromArg(arg).forEach(
-                        (method) -> beforeActions.add(wrap(method, dryRun))),
+                arg -> getJavaMethodsFromArg(arg).map(
+                        this::wrap).forEachOrdered(beforeActions::add),
+
                 CMDLINE_ARG_PREFIX + "run",
-                arg -> flushTestGroup(getJavaMethodsFromArg(arg).map(
-                        TestBuilder::toMethodCalls).flatMap(List::stream).collect(
+                arg -> addTestGroup(getJavaMethodsFromArg(arg).map(
+                        ThrowingFunction.toFunction(
+                                TestBuilder::toMethodCalls)).flatMap(s -> s).collect(
                         Collectors.toList())),
+
+                CMDLINE_ARG_PREFIX + "exclude",
+                arg -> (excludedTests = Optional.ofNullable(
+                        excludedTests).orElse(new HashSet<String>())).add(arg),
+
+                CMDLINE_ARG_PREFIX + "include",
+                arg -> (includedTests = Optional.ofNullable(
+                        includedTests).orElse(new HashSet<String>())).add(arg),
+
+                CMDLINE_ARG_PREFIX + "space-subst",
+                arg -> spaceSubstitute = arg,
+
+                CMDLINE_ARG_PREFIX + "group",
+                arg -> flushTestGroup(),
+
                 CMDLINE_ARG_PREFIX + "dry-run",
-                arg -> dryRun = true);
+                arg -> dryRun = true
+        );
         this.testConsumer = testConsumer;
         clear();
     }
@@ -87,50 +110,93 @@
         }
     }
 
-    private void flushTestGroup(List<MethodCall> newTestGroup) {
+    private void addTestGroup(List<MethodCall> newTestGroup) {
         if (testGroup != null) {
-            testGroup.forEach(testBody -> createTestInstance(testBody));
+            testGroup.addAll(newTestGroup);
+        } else {
+            testGroup = newTestGroup;
+        }
+    }
+
+    private static Stream<MethodCall> filterTests(Stream<MethodCall> tests,
+            Set<String> filters, UnaryOperator<Boolean> pred, String logMsg) {
+        if (filters == null) {
+            return tests;
+        }
+
+        // Log all matches before returning from the function
+        return tests.filter(test -> {
+            String testDescription = test.createDescription().testFullName();
+            boolean match = filters.stream().anyMatch(
+                    v -> testDescription.contains(v));
+            if (match) {
+                trace(String.format(logMsg + ": %s", testDescription));
+            }
+            return pred.apply(match);
+        }).collect(Collectors.toList()).stream();
+    }
+
+    private Stream<MethodCall> filterTestGroup() {
+        Objects.requireNonNull(testGroup);
+
+        UnaryOperator<Set<String>> restoreSpaces = filters -> {
+            if (spaceSubstitute == null || filters == null) {
+                return filters;
+            }
+            return filters.stream().map(
+                    filter -> filter.replace(spaceSubstitute, " ")).collect(
+                            Collectors.toSet());
+        };
+
+        if (includedTests != null) {
+            return filterTests(testGroup.stream(), restoreSpaces.apply(
+                    includedTests), x -> x, "Include");
+        }
+
+        return filterTests(testGroup.stream(),
+                restoreSpaces.apply(excludedTests), x -> !x, "Exclude");
+    }
+
+    private void flushTestGroup() {
+        if (testGroup != null) {
+            filterTestGroup().forEach(testBody -> createTestInstance(testBody));
             clear();
         }
-        testGroup = newTestGroup;
     }
 
     private void createTestInstance(MethodCall testBody) {
-        ThrowingFunction<MethodCall, Object> testContructor;
-        if (dryRun) {
-            testContructor = (unused) -> null;
-            testBody = DRY_RUN_TEST_BODY;
+        final List<ThrowingConsumer> curBeforeActions;
+        final List<ThrowingConsumer> curAfterActions;
+
+        Method testMethod = testBody.getMethod();
+        if (Stream.of(BeforeEach.class, AfterEach.class).anyMatch(
+                type -> testMethod.isAnnotationPresent(type))) {
+            curBeforeActions = beforeActions;
+            curAfterActions = afterActions;
         } else {
-            testContructor = TestBuilder::constructTest;
+            curBeforeActions = new ArrayList<>(beforeActions);
+            curAfterActions = new ArrayList<>(afterActions);
+
+            selectFrameMethods(testMethod.getDeclaringClass(), BeforeEach.class).map(
+                    this::wrap).forEachOrdered(curBeforeActions::add);
+            selectFrameMethods(testMethod.getDeclaringClass(), AfterEach.class).map(
+                    this::wrap).forEachOrdered(curAfterActions::add);
         }
 
-        TestInstance test = new TestInstance(testContructor, testBody,
-                beforeActions, afterActions);
-        trace(String.format("[%s] test constructed", test.fullName()));
+        TestInstance test = new TestInstance(testBody, curBeforeActions,
+                curAfterActions, dryRun);
+        if (includedTests == null) {
+            trace(String.format("Create: %s", test.fullName()));
+        }
         testConsumer.accept(test);
     }
 
-    public static void nop () {
-    }
-
-    private final static MethodCall DRY_RUN_TEST_BODY = ThrowingSupplier.toSupplier(() -> {
-        return new MethodCall(TestBuilder.class.getMethod("nop"));
-    }).get();
-
-    private static Object constructTest(MethodCall testBody) throws
-            NoSuchMethodException, InstantiationException,
-            IllegalAccessException, IllegalArgumentException,
-            InvocationTargetException {
-        Constructor ctor = testBody.getRequiredConstructor();
-        if (ctor == null) {
-            return null;
-        }
-        return ctor.newInstance();
-    }
-
     private void clear() {
         beforeActions = new ArrayList<>();
         afterActions = new ArrayList<>();
+        excludedTests = null;
+        includedTests = null;
+        spaceSubstitute = null;
         testGroup = null;
     }
 
@@ -142,6 +208,14 @@
         }
     }
 
+    private static Stream<Method> selectFrameMethods(Class type, Class annotationType) {
+        return Stream.of(type.getMethods())
+                .filter(m -> m.getParameterCount() == 0)
+                .filter(m -> !m.isAnnotationPresent(Test.class))
+                .filter(m -> m.isAnnotationPresent(annotationType))
+                .sorted((a, b) -> a.getName().compareTo(b.getName()));
+    }
+
     private static Stream<String> cmdLineArgValueToMethodNames(String v) {
         List<String> result = new ArrayList<>();
         String defaultClassName = null;
@@ -156,6 +230,7 @@
                         m -> m.isAnnotationPresent(Test.class)).map(
                                 Method::getName).distinct().forEach(
                                 name -> result.add(String.join(".", token, name)));
+
                 continue;
             }
 
@@ -188,7 +263,7 @@
     }
 
     private static boolean isParametrized(Method method) {
-        return method.isAnnotationPresent(Parameters.class) || method.isAnnotationPresent(
+        return method.isAnnotationPresent(ParameterGroup.class) || method.isAnnotationPresent(
                 Parameter.class);
     }
 
@@ -215,19 +290,6 @@
                     "Method [%s] not found in [%s] class;",
                     methodName, className));
         }
-        // Make sure default constructor is accessible if the one is needed.
-        // Need to probe all methods as some of them might be static and
-        // some class members.
-        // Onlu class members require default ctor.
-        for (Method method : methods) {
-            try {
-                MethodCall.getRequiredConstructor(method);
-            } catch (NoSuchMethodException ex) {
-                throw new ParseException(String.format(
-                        "Default constructor not found in [%s] class;",
-                        className));
-            }
-        }
 
         trace(String.format("%s -> %s", qualifiedMethodName, methods));
         return methods;
@@ -240,9 +302,9 @@
                         List::stream).sequential();
     }
 
-    private static Parameter[] getParameters(Method method) {
-        if (method.isAnnotationPresent(Parameters.class)) {
-            return ((Parameters) method.getAnnotation(Parameters.class)).value();
+    private static Parameter[] getMethodParameters(Method method) {
+        if (method.isAnnotationPresent(ParameterGroup.class)) {
+            return ((ParameterGroup) method.getAnnotation(ParameterGroup.class)).value();
         }
 
         if (method.isAnnotationPresent(Parameter.class)) {
@@ -254,20 +316,73 @@
         return null;
     }
 
-    private static List<MethodCall> toMethodCalls(Method method) {
-        if (!isParametrized(method)) {
-            return List.of(new MethodCall(method));
+    private static Stream<Object[]> toCtorArgs(Method method) throws
+            IllegalAccessException, InvocationTargetException {
+        Class type = method.getDeclaringClass();
+        List<Method> paremetersProviders = Stream.of(type.getMethods())
+                .filter(m -> m.getParameterCount() == 0)
+                .filter(m -> (m.getModifiers() & Modifier.STATIC) != 0)
+                .filter(m -> m.isAnnotationPresent(Parameters.class))
+                .sorted()
+                .collect(Collectors.toList());
+        if (paremetersProviders.isEmpty()) {
+            // Single instance using the default constructor.
+            return Stream.ofNullable(MethodCall.DEFAULT_CTOR_ARGS);
+        }
+
+        // Pick the first method from the list.
+        Method paremetersProvider = paremetersProviders.iterator().next();
+        if (paremetersProviders.size() > 1) {
+            trace(String.format(
+                    "Found %d public static methods without arguments with %s annotation. Will use %s",
+                    paremetersProviders.size(), Parameters.class,
+                    paremetersProvider));
+            paremetersProviders.stream().map(Method::toString).forEach(
+                    TestBuilder::trace);
         }
-        Parameter[] annotations = getParameters(method);
+
+        // Construct collection of arguments for test class instances.
+        return ((Collection) paremetersProvider.invoke(null)).stream();
+    }
+
+    private static Stream<MethodCall> toMethodCalls(Method method) throws
+            IllegalAccessException, InvocationTargetException {
+        return toCtorArgs(method).map(v -> toMethodCalls(v, method)).flatMap(
+                s -> s).peek(methodCall -> {
+                    // Make sure required constructor is accessible if the one is needed.
+                    // Need to probe all methods as some of them might be static
+                    // and some class members.
+                    // Only class members require ctors.
+                    try {
+                        methodCall.checkRequiredConstructor();
+                    } catch (NoSuchMethodException ex) {
+                        throw new ParseException(ex.getMessage() + ".");
+                    }
+                });
+    }
+
+    private static Stream<MethodCall> toMethodCalls(Object[] ctorArgs, Method method) {
+        if (!isParametrized(method)) {
+            return Stream.of(new MethodCall(ctorArgs, method));
+        }
+        Parameter[] annotations = getMethodParameters(method);
         if (annotations.length == 0) {
-            return List.of(new MethodCall(method));
+            return Stream.of(new MethodCall(ctorArgs, method));
         }
         return Stream.of(annotations).map((a) -> {
-            String annotationValue = a.value();
-            Class paramClass = method.getParameterTypes()[0];
-            return new MethodCall(method,
-                    fromString(annotationValue, paramClass));
-        }).collect(Collectors.toList());
+            Class paramType = method.getParameterTypes()[0];
+            final Object annotationValue;
+            if (!paramType.isArray()) {
+                annotationValue = fromString(a.value()[0], paramType);
+            } else {
+                Class paramComponentType = paramType.getComponentType();
+                annotationValue = Array.newInstance(paramComponentType, a.value().length);
+                var idx = new AtomicInteger(-1);
+                Stream.of(a.value()).map(v -> fromString(v, paramComponentType)).sequential().forEach(
+                        v -> Array.set(annotationValue, idx.incrementAndGet(), v));
+            }
+            return new MethodCall(ctorArgs, method, annotationValue);
+        });
     }
 
     private static Object fromString(String value, Class toType) {
@@ -281,7 +396,7 @@
     }
 
     // Wraps Method.invike() into ThrowingRunnable.run()
-    private static ThrowingConsumer wrap(Method method, boolean dryRun) {
+    private ThrowingConsumer wrap(Method method) {
         return (test) -> {
             Class methodClass = method.getDeclaringClass();
             String methodName = String.join(".", methodClass.getName(),
@@ -318,7 +433,7 @@
         private String badCmdLineArg;
     }
 
-    private static void trace(String msg) {
+    static void trace(String msg) {
         if (TKit.VERBOSE_TEST_SETUP) {
             TKit.log(msg);
         }
@@ -329,6 +444,9 @@
     private List<MethodCall> testGroup;
     private List<ThrowingConsumer> beforeActions;
     private List<ThrowingConsumer> afterActions;
+    private Set<String> excludedTests;
+    private Set<String> includedTests;
+    private String spaceSubstitute;
     private boolean dryRun;
 
     private final static Map<Class, Function<String, Object>> conv = Map.of(