test/jdk/tools/jpackage/helpers/jdk/jpackage/test/TestBuilder.java
branchJDK-8200758-branch
changeset 58416 f09bf58c1f17
child 58648 3bf53ffa9ae7
equal deleted inserted replaced
58415:73f8e557549a 58416:f09bf58c1f17
       
     1 /*
       
     2  * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved.
       
     3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
       
     4  *
       
     5  * This code is free software; you can redistribute it and/or modify it
       
     6  * under the terms of the GNU General Public License version 2 only, as
       
     7  * published by the Free Software Foundation.
       
     8  *
       
     9  * This code is distributed in the hope that it will be useful, but WITHOUT
       
    10  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
       
    11  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
       
    12  * version 2 for more details (a copy is included in the LICENSE file that
       
    13  * accompanied this code).
       
    14  *
       
    15  * You should have received a copy of the GNU General Public License version
       
    16  * 2 along with this work; if not, write to the Free Software Foundation,
       
    17  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
       
    18  *
       
    19  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
       
    20  * or visit www.oracle.com if you need additional information or have any
       
    21  * questions.
       
    22  */
       
    23 
       
    24 package jdk.jpackage.test;
       
    25 
       
    26 import java.lang.reflect.Constructor;
       
    27 import java.lang.reflect.InvocationTargetException;
       
    28 import java.lang.reflect.Method;
       
    29 import java.util.ArrayList;
       
    30 import java.util.List;
       
    31 import java.util.Map;
       
    32 import java.util.function.Consumer;
       
    33 import java.util.function.Function;
       
    34 import java.util.stream.Collectors;
       
    35 import java.util.stream.Stream;
       
    36 import jdk.jpackage.test.Annotations.Parameter;
       
    37 import jdk.jpackage.test.Annotations.Parameters;
       
    38 import jdk.jpackage.test.Annotations.Test;
       
    39 import jdk.jpackage.test.Functional.ThrowingConsumer;
       
    40 import jdk.jpackage.test.Functional.ThrowingFunction;
       
    41 import jdk.jpackage.test.Functional.ThrowingSupplier;
       
    42 
       
    43 final class TestBuilder implements AutoCloseable {
       
    44 
       
    45     @Override
       
    46     public void close() throws Exception {
       
    47         flushTestGroup(null);
       
    48     }
       
    49 
       
    50     TestBuilder(Consumer<TestInstance> testConsumer) {
       
    51         argProcessors = Map.of(CMDLINE_ARG_PREFIX + "after-run",
       
    52                 arg -> getJavaMethodsFromArg(arg).forEach(
       
    53                         (method) -> afterActions.add(wrap(method, dryRun))),
       
    54                 CMDLINE_ARG_PREFIX + "before-run",
       
    55                 arg -> getJavaMethodsFromArg(arg).forEach(
       
    56                         (method) -> beforeActions.add(wrap(method, dryRun))),
       
    57                 CMDLINE_ARG_PREFIX + "run",
       
    58                 arg -> flushTestGroup(getJavaMethodsFromArg(arg).map(
       
    59                         TestBuilder::toMethodCalls).flatMap(List::stream).collect(
       
    60                         Collectors.toList())),
       
    61                 CMDLINE_ARG_PREFIX + "dry-run",
       
    62                 arg -> dryRun = true);
       
    63         this.testConsumer = testConsumer;
       
    64         clear();
       
    65     }
       
    66 
       
    67     void processCmdLineArg(String arg) throws Throwable {
       
    68         int separatorIdx = arg.indexOf('=');
       
    69         final String argName;
       
    70         final String argValue;
       
    71         if (separatorIdx != -1) {
       
    72             argName = arg.substring(0, separatorIdx);
       
    73             argValue = arg.substring(separatorIdx + 1);
       
    74         } else {
       
    75             argName = arg;
       
    76             argValue = null;
       
    77         }
       
    78         try {
       
    79             ThrowingConsumer<String> argProcessor = argProcessors.get(argName);
       
    80             if (argProcessor == null) {
       
    81                 throw new ParseException("Unrecognized");
       
    82             }
       
    83             argProcessor.accept(argValue);
       
    84         } catch (ParseException ex) {
       
    85             ex.setContext(arg);
       
    86             throw ex;
       
    87         }
       
    88     }
       
    89 
       
    90     private void flushTestGroup(List<MethodCall> newTestGroup) {
       
    91         if (testGroup != null) {
       
    92             testGroup.forEach(testBody -> createTestInstance(testBody));
       
    93             clear();
       
    94         }
       
    95         testGroup = newTestGroup;
       
    96     }
       
    97 
       
    98     private void createTestInstance(MethodCall testBody) {
       
    99         ThrowingFunction<MethodCall, Object> testContructor;
       
   100         if (dryRun) {
       
   101             testContructor = (unused) -> null;
       
   102             testBody = DRY_RUN_TEST_BODY;
       
   103         } else {
       
   104             testContructor = TestBuilder::constructTest;
       
   105         }
       
   106 
       
   107         TestInstance test = new TestInstance(testContructor, testBody,
       
   108                 beforeActions, afterActions);
       
   109         trace(String.format("[%s] test constructed", test.fullName()));
       
   110         testConsumer.accept(test);
       
   111     }
       
   112 
       
   113     public static void nop () {
       
   114     }
       
   115 
       
   116     private final static MethodCall DRY_RUN_TEST_BODY = ThrowingSupplier.toSupplier(() -> {
       
   117         return new MethodCall(TestBuilder.class.getMethod("nop"));
       
   118     }).get();
       
   119 
       
   120     private static Object constructTest(MethodCall testBody) throws
       
   121             NoSuchMethodException, InstantiationException,
       
   122             IllegalAccessException, IllegalArgumentException,
       
   123             InvocationTargetException {
       
   124         Constructor ctor = testBody.getRequiredConstructor();
       
   125         if (ctor == null) {
       
   126             return null;
       
   127         }
       
   128         return ctor.newInstance();
       
   129     }
       
   130 
       
   131     private void clear() {
       
   132         beforeActions = new ArrayList<>();
       
   133         afterActions = new ArrayList<>();
       
   134         testGroup = null;
       
   135     }
       
   136 
       
   137     private static Class probeClass(String name) {
       
   138         try {
       
   139             return Class.forName(name);
       
   140         } catch (ClassNotFoundException ex) {
       
   141             return null;
       
   142         }
       
   143     }
       
   144 
       
   145     private static Stream<String> cmdLineArgValueToMethodNames(String v) {
       
   146         List<String> result = new ArrayList<>();
       
   147         String defaultClassName = null;
       
   148         for (String token : v.split(",")) {
       
   149             Class testSet = probeClass(token);
       
   150             if (testSet != null) {
       
   151                 // Test set class specified. Pull in all public methods
       
   152                 // from the class with @Test annotation removing name duplicates.
       
   153                 // Overloads will be handled at the next phase of processing.
       
   154                 defaultClassName = token;
       
   155                 Stream.of(testSet.getMethods()).filter(
       
   156                         m -> m.isAnnotationPresent(Test.class)).map(
       
   157                                 Method::getName).distinct().forEach(
       
   158                                 name -> result.add(String.join(".", token, name)));
       
   159                 continue;
       
   160             }
       
   161 
       
   162             final String qualifiedMethodName;
       
   163             final int lastDotIdx = token.lastIndexOf('.');
       
   164             if (lastDotIdx != -1) {
       
   165                 qualifiedMethodName = token;
       
   166                 defaultClassName = token.substring(0, lastDotIdx);
       
   167             } else if (defaultClassName == null) {
       
   168                 throw new ParseException("Default class name not found in");
       
   169             } else {
       
   170                 qualifiedMethodName = String.join(".", defaultClassName, token);
       
   171             }
       
   172             result.add(qualifiedMethodName);
       
   173         }
       
   174         return result.stream();
       
   175     }
       
   176 
       
   177     private static boolean filterMethod(String expectedMethodName, Method method) {
       
   178         if (!method.getName().equals(expectedMethodName)) {
       
   179             return false;
       
   180         }
       
   181         switch (method.getParameterCount()) {
       
   182             case 0:
       
   183                 return !isParametrized(method);
       
   184             case 1:
       
   185                 return isParametrized(method);
       
   186         }
       
   187         return false;
       
   188     }
       
   189 
       
   190     private static boolean isParametrized(Method method) {
       
   191         return method.isAnnotationPresent(Parameters.class) || method.isAnnotationPresent(
       
   192                 Parameter.class);
       
   193     }
       
   194 
       
   195     private static List<Method> getJavaMethodFromString(
       
   196             String qualifiedMethodName) {
       
   197         int lastDotIdx = qualifiedMethodName.lastIndexOf('.');
       
   198         if (lastDotIdx == -1) {
       
   199             throw new ParseException("Class name not found in");
       
   200         }
       
   201         String className = qualifiedMethodName.substring(0, lastDotIdx);
       
   202         String methodName = qualifiedMethodName.substring(lastDotIdx + 1);
       
   203         Class methodClass;
       
   204         try {
       
   205             methodClass = Class.forName(className);
       
   206         } catch (ClassNotFoundException ex) {
       
   207             throw new ParseException(String.format("Class [%s] not found;",
       
   208                     className));
       
   209         }
       
   210         // Get the list of all public methods as need to deal with overloads.
       
   211         List<Method> methods = Stream.of(methodClass.getMethods()).filter(
       
   212                 (m) -> filterMethod(methodName, m)).collect(Collectors.toList());
       
   213         if (methods.isEmpty()) {
       
   214             new ParseException(String.format(
       
   215                     "Method [%s] not found in [%s] class;",
       
   216                     methodName, className));
       
   217         }
       
   218         // Make sure default constructor is accessible if the one is needed.
       
   219         // Need to probe all methods as some of them might be static and
       
   220         // some class members.
       
   221         // Onlu class members require default ctor.
       
   222         for (Method method : methods) {
       
   223             try {
       
   224                 MethodCall.getRequiredConstructor(method);
       
   225             } catch (NoSuchMethodException ex) {
       
   226                 throw new ParseException(String.format(
       
   227                         "Default constructor not found in [%s] class;",
       
   228                         className));
       
   229             }
       
   230         }
       
   231 
       
   232         trace(String.format("%s -> %s", qualifiedMethodName, methods));
       
   233         return methods;
       
   234     }
       
   235 
       
   236     private static Stream<Method> getJavaMethodsFromArg(String argValue) {
       
   237         return cmdLineArgValueToMethodNames(argValue).map(
       
   238                 ThrowingFunction.toFunction(
       
   239                         TestBuilder::getJavaMethodFromString)).flatMap(
       
   240                         List::stream).sequential();
       
   241     }
       
   242 
       
   243     private static Parameter[] getParameters(Method method) {
       
   244         if (method.isAnnotationPresent(Parameters.class)) {
       
   245             return ((Parameters) method.getAnnotation(Parameters.class)).value();
       
   246         }
       
   247 
       
   248         if (method.isAnnotationPresent(Parameter.class)) {
       
   249             return new Parameter[]{(Parameter) method.getAnnotation(
       
   250                 Parameter.class)};
       
   251         }
       
   252 
       
   253         // Unexpected
       
   254         return null;
       
   255     }
       
   256 
       
   257     private static List<MethodCall> toMethodCalls(Method method) {
       
   258         if (!isParametrized(method)) {
       
   259             return List.of(new MethodCall(method));
       
   260         }
       
   261         Parameter[] annotations = getParameters(method);
       
   262         if (annotations.length == 0) {
       
   263             return List.of(new MethodCall(method));
       
   264         }
       
   265         return Stream.of(annotations).map((a) -> {
       
   266             String annotationValue = a.value();
       
   267             Class paramClass = method.getParameterTypes()[0];
       
   268             return new MethodCall(method,
       
   269                     fromString(annotationValue, paramClass));
       
   270         }).collect(Collectors.toList());
       
   271     }
       
   272 
       
   273     private static Object fromString(String value, Class toType) {
       
   274         Function<String, Object> converter = conv.get(toType);
       
   275         if (converter == null) {
       
   276             throw new RuntimeException(String.format(
       
   277                     "Failed to find a conversion of [%s] string to %s type",
       
   278                     value, toType));
       
   279         }
       
   280         return converter.apply(value);
       
   281     }
       
   282 
       
   283     // Wraps Method.invike() into ThrowingRunnable.run()
       
   284     private static ThrowingConsumer wrap(Method method, boolean dryRun) {
       
   285         return (test) -> {
       
   286             Class methodClass = method.getDeclaringClass();
       
   287             String methodName = String.join(".", methodClass.getName(),
       
   288                     method.getName());
       
   289             TKit.log(String.format("[ CALL     ] %s()", methodName));
       
   290             if (!dryRun) {
       
   291                 if (methodClass.isInstance(test)) {
       
   292                     method.invoke(test);
       
   293                 } else {
       
   294                     method.invoke(null);
       
   295                 }
       
   296             }
       
   297         };
       
   298     }
       
   299 
       
   300     private static class ParseException extends IllegalArgumentException {
       
   301 
       
   302         ParseException(String msg) {
       
   303             super(msg);
       
   304         }
       
   305 
       
   306         void setContext(String badCmdLineArg) {
       
   307             this.badCmdLineArg = badCmdLineArg;
       
   308         }
       
   309 
       
   310         @Override
       
   311         public String getMessage() {
       
   312             String msg = super.getMessage();
       
   313             if (badCmdLineArg != null) {
       
   314                 msg = String.format("%s parameter=[%s]", msg, badCmdLineArg);
       
   315             }
       
   316             return msg;
       
   317         }
       
   318         private String badCmdLineArg;
       
   319     }
       
   320 
       
   321     private static void trace(String msg) {
       
   322         if (TKit.VERBOSE_TEST_SETUP) {
       
   323             TKit.log(msg);
       
   324         }
       
   325     }
       
   326 
       
   327     private final Map<String, ThrowingConsumer<String>> argProcessors;
       
   328     private Consumer<TestInstance> testConsumer;
       
   329     private List<MethodCall> testGroup;
       
   330     private List<ThrowingConsumer> beforeActions;
       
   331     private List<ThrowingConsumer> afterActions;
       
   332     private boolean dryRun;
       
   333 
       
   334     private final static Map<Class, Function<String, Object>> conv = Map.of(
       
   335             boolean.class, Boolean::valueOf,
       
   336             Boolean.class, Boolean::valueOf,
       
   337             int.class, Integer::valueOf,
       
   338             Integer.class, Integer::valueOf,
       
   339             long.class, Long::valueOf,
       
   340             Long.class, Long::valueOf,
       
   341             String.class, String::valueOf);
       
   342 
       
   343     final static String CMDLINE_ARG_PREFIX = "--jpt-";
       
   344 }