test/jdk/tools/jpackage/helpers/jdk/jpackage/test/TestBuilder.java
branchJDK-8200758-branch
changeset 58416 f09bf58c1f17
child 58648 3bf53ffa9ae7
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/TestBuilder.java	Mon Sep 30 19:11:19 2019 -0400
@@ -0,0 +1,344 @@
+/*
+ * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package jdk.jpackage.test;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import jdk.jpackage.test.Annotations.Parameter;
+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);
+    }
+
+    TestBuilder(Consumer<TestInstance> testConsumer) {
+        argProcessors = Map.of(CMDLINE_ARG_PREFIX + "after-run",
+                arg -> getJavaMethodsFromArg(arg).forEach(
+                        (method) -> afterActions.add(wrap(method, dryRun))),
+                CMDLINE_ARG_PREFIX + "before-run",
+                arg -> getJavaMethodsFromArg(arg).forEach(
+                        (method) -> beforeActions.add(wrap(method, dryRun))),
+                CMDLINE_ARG_PREFIX + "run",
+                arg -> flushTestGroup(getJavaMethodsFromArg(arg).map(
+                        TestBuilder::toMethodCalls).flatMap(List::stream).collect(
+                        Collectors.toList())),
+                CMDLINE_ARG_PREFIX + "dry-run",
+                arg -> dryRun = true);
+        this.testConsumer = testConsumer;
+        clear();
+    }
+
+    void processCmdLineArg(String arg) throws Throwable {
+        int separatorIdx = arg.indexOf('=');
+        final String argName;
+        final String argValue;
+        if (separatorIdx != -1) {
+            argName = arg.substring(0, separatorIdx);
+            argValue = arg.substring(separatorIdx + 1);
+        } else {
+            argName = arg;
+            argValue = null;
+        }
+        try {
+            ThrowingConsumer<String> argProcessor = argProcessors.get(argName);
+            if (argProcessor == null) {
+                throw new ParseException("Unrecognized");
+            }
+            argProcessor.accept(argValue);
+        } catch (ParseException ex) {
+            ex.setContext(arg);
+            throw ex;
+        }
+    }
+
+    private void flushTestGroup(List<MethodCall> newTestGroup) {
+        if (testGroup != null) {
+            testGroup.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;
+        } else {
+            testContructor = TestBuilder::constructTest;
+        }
+
+        TestInstance test = new TestInstance(testContructor, testBody,
+                beforeActions, afterActions);
+        trace(String.format("[%s] test constructed", 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<>();
+        testGroup = null;
+    }
+
+    private static Class probeClass(String name) {
+        try {
+            return Class.forName(name);
+        } catch (ClassNotFoundException ex) {
+            return null;
+        }
+    }
+
+    private static Stream<String> cmdLineArgValueToMethodNames(String v) {
+        List<String> result = new ArrayList<>();
+        String defaultClassName = null;
+        for (String token : v.split(",")) {
+            Class testSet = probeClass(token);
+            if (testSet != null) {
+                // Test set class specified. Pull in all public methods
+                // from the class with @Test annotation removing name duplicates.
+                // Overloads will be handled at the next phase of processing.
+                defaultClassName = token;
+                Stream.of(testSet.getMethods()).filter(
+                        m -> m.isAnnotationPresent(Test.class)).map(
+                                Method::getName).distinct().forEach(
+                                name -> result.add(String.join(".", token, name)));
+                continue;
+            }
+
+            final String qualifiedMethodName;
+            final int lastDotIdx = token.lastIndexOf('.');
+            if (lastDotIdx != -1) {
+                qualifiedMethodName = token;
+                defaultClassName = token.substring(0, lastDotIdx);
+            } else if (defaultClassName == null) {
+                throw new ParseException("Default class name not found in");
+            } else {
+                qualifiedMethodName = String.join(".", defaultClassName, token);
+            }
+            result.add(qualifiedMethodName);
+        }
+        return result.stream();
+    }
+
+    private static boolean filterMethod(String expectedMethodName, Method method) {
+        if (!method.getName().equals(expectedMethodName)) {
+            return false;
+        }
+        switch (method.getParameterCount()) {
+            case 0:
+                return !isParametrized(method);
+            case 1:
+                return isParametrized(method);
+        }
+        return false;
+    }
+
+    private static boolean isParametrized(Method method) {
+        return method.isAnnotationPresent(Parameters.class) || method.isAnnotationPresent(
+                Parameter.class);
+    }
+
+    private static List<Method> getJavaMethodFromString(
+            String qualifiedMethodName) {
+        int lastDotIdx = qualifiedMethodName.lastIndexOf('.');
+        if (lastDotIdx == -1) {
+            throw new ParseException("Class name not found in");
+        }
+        String className = qualifiedMethodName.substring(0, lastDotIdx);
+        String methodName = qualifiedMethodName.substring(lastDotIdx + 1);
+        Class methodClass;
+        try {
+            methodClass = Class.forName(className);
+        } catch (ClassNotFoundException ex) {
+            throw new ParseException(String.format("Class [%s] not found;",
+                    className));
+        }
+        // Get the list of all public methods as need to deal with overloads.
+        List<Method> methods = Stream.of(methodClass.getMethods()).filter(
+                (m) -> filterMethod(methodName, m)).collect(Collectors.toList());
+        if (methods.isEmpty()) {
+            new ParseException(String.format(
+                    "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;
+    }
+
+    private static Stream<Method> getJavaMethodsFromArg(String argValue) {
+        return cmdLineArgValueToMethodNames(argValue).map(
+                ThrowingFunction.toFunction(
+                        TestBuilder::getJavaMethodFromString)).flatMap(
+                        List::stream).sequential();
+    }
+
+    private static Parameter[] getParameters(Method method) {
+        if (method.isAnnotationPresent(Parameters.class)) {
+            return ((Parameters) method.getAnnotation(Parameters.class)).value();
+        }
+
+        if (method.isAnnotationPresent(Parameter.class)) {
+            return new Parameter[]{(Parameter) method.getAnnotation(
+                Parameter.class)};
+        }
+
+        // Unexpected
+        return null;
+    }
+
+    private static List<MethodCall> toMethodCalls(Method method) {
+        if (!isParametrized(method)) {
+            return List.of(new MethodCall(method));
+        }
+        Parameter[] annotations = getParameters(method);
+        if (annotations.length == 0) {
+            return List.of(new MethodCall(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());
+    }
+
+    private static Object fromString(String value, Class toType) {
+        Function<String, Object> converter = conv.get(toType);
+        if (converter == null) {
+            throw new RuntimeException(String.format(
+                    "Failed to find a conversion of [%s] string to %s type",
+                    value, toType));
+        }
+        return converter.apply(value);
+    }
+
+    // Wraps Method.invike() into ThrowingRunnable.run()
+    private static ThrowingConsumer wrap(Method method, boolean dryRun) {
+        return (test) -> {
+            Class methodClass = method.getDeclaringClass();
+            String methodName = String.join(".", methodClass.getName(),
+                    method.getName());
+            TKit.log(String.format("[ CALL     ] %s()", methodName));
+            if (!dryRun) {
+                if (methodClass.isInstance(test)) {
+                    method.invoke(test);
+                } else {
+                    method.invoke(null);
+                }
+            }
+        };
+    }
+
+    private static class ParseException extends IllegalArgumentException {
+
+        ParseException(String msg) {
+            super(msg);
+        }
+
+        void setContext(String badCmdLineArg) {
+            this.badCmdLineArg = badCmdLineArg;
+        }
+
+        @Override
+        public String getMessage() {
+            String msg = super.getMessage();
+            if (badCmdLineArg != null) {
+                msg = String.format("%s parameter=[%s]", msg, badCmdLineArg);
+            }
+            return msg;
+        }
+        private String badCmdLineArg;
+    }
+
+    private static void trace(String msg) {
+        if (TKit.VERBOSE_TEST_SETUP) {
+            TKit.log(msg);
+        }
+    }
+
+    private final Map<String, ThrowingConsumer<String>> argProcessors;
+    private Consumer<TestInstance> testConsumer;
+    private List<MethodCall> testGroup;
+    private List<ThrowingConsumer> beforeActions;
+    private List<ThrowingConsumer> afterActions;
+    private boolean dryRun;
+
+    private final static Map<Class, Function<String, Object>> conv = Map.of(
+            boolean.class, Boolean::valueOf,
+            Boolean.class, Boolean::valueOf,
+            int.class, Integer::valueOf,
+            Integer.class, Integer::valueOf,
+            long.class, Long::valueOf,
+            Long.class, Long::valueOf,
+            String.class, String::valueOf);
+
+    final static String CMDLINE_ARG_PREFIX = "--jpt-";
+}