--- /dev/null 2019-11-13 18:33:49.000000000 -0500 +++ new/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/TestBuilder.java 2019-11-13 18:33:46.478222900 -0500 @@ -0,0 +1,462 @@ +/* + * 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.Array; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +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; + +final class TestBuilder implements AutoCloseable { + + @Override + public void close() throws Exception { + flushTestGroup(); + } + + TestBuilder(Consumer testConsumer) { + 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).map( + this::wrap).forEachOrdered(beforeActions::add), + + CMDLINE_ARG_PREFIX + "run", + 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())).add(arg), + + CMDLINE_ARG_PREFIX + "include", + arg -> (includedTests = Optional.ofNullable( + includedTests).orElse(new HashSet())).add(arg), + + CMDLINE_ARG_PREFIX + "space-subst", + arg -> spaceSubstitute = arg, + + CMDLINE_ARG_PREFIX + "group", + arg -> flushTestGroup(), + + 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 argProcessor = argProcessors.get(argName); + if (argProcessor == null) { + throw new ParseException("Unrecognized"); + } + argProcessor.accept(argValue); + } catch (ParseException ex) { + ex.setContext(arg); + throw ex; + } + } + + private void addTestGroup(List newTestGroup) { + if (testGroup != null) { + testGroup.addAll(newTestGroup); + } else { + testGroup = newTestGroup; + } + } + + private static Stream filterTests(Stream tests, + Set filters, UnaryOperator 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 filterTestGroup() { + Objects.requireNonNull(testGroup); + + UnaryOperator> 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(); + } + } + + private void createTestInstance(MethodCall testBody) { + final List curBeforeActions; + final List curAfterActions; + + Method testMethod = testBody.getMethod(); + if (Stream.of(BeforeEach.class, AfterEach.class).anyMatch( + type -> testMethod.isAnnotationPresent(type))) { + curBeforeActions = beforeActions; + curAfterActions = afterActions; + } else { + 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(testBody, curBeforeActions, + curAfterActions, dryRun); + if (includedTests == null) { + trace(String.format("Create: %s", test.fullName())); + } + testConsumer.accept(test); + } + + private void clear() { + beforeActions = new ArrayList<>(); + afterActions = new ArrayList<>(); + excludedTests = null; + includedTests = null; + spaceSubstitute = null; + testGroup = null; + } + + private static Class probeClass(String name) { + try { + return Class.forName(name); + } catch (ClassNotFoundException ex) { + return null; + } + } + + private static Stream 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 cmdLineArgValueToMethodNames(String v) { + List 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(ParameterGroup.class) || method.isAnnotationPresent( + Parameter.class); + } + + private static List 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 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)); + } + + trace(String.format("%s -> %s", qualifiedMethodName, methods)); + return methods; + } + + private static Stream getJavaMethodsFromArg(String argValue) { + return cmdLineArgValueToMethodNames(argValue).map( + ThrowingFunction.toFunction( + TestBuilder::getJavaMethodFromString)).flatMap( + List::stream).sequential(); + } + + private static Parameter[] getMethodParameters(Method method) { + if (method.isAnnotationPresent(ParameterGroup.class)) { + return ((ParameterGroup) method.getAnnotation(ParameterGroup.class)).value(); + } + + if (method.isAnnotationPresent(Parameter.class)) { + return new Parameter[]{(Parameter) method.getAnnotation( + Parameter.class)}; + } + + // Unexpected + return null; + } + + private static Stream toCtorArgs(Method method) throws + IllegalAccessException, InvocationTargetException { + Class type = method.getDeclaringClass(); + List 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); + } + + // Construct collection of arguments for test class instances. + return ((Collection) paremetersProvider.invoke(null)).stream(); + } + + private static Stream 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 toMethodCalls(Object[] ctorArgs, Method method) { + if (!isParametrized(method)) { + return Stream.of(new MethodCall(ctorArgs, method)); + } + Parameter[] annotations = getMethodParameters(method); + if (annotations.length == 0) { + return Stream.of(new MethodCall(ctorArgs, method)); + } + return Stream.of(annotations).map((a) -> { + 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) { + Function 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 ThrowingConsumer wrap(Method method) { + 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; + } + + static void trace(String msg) { + if (TKit.VERBOSE_TEST_SETUP) { + TKit.log(msg); + } + } + + private final Map> argProcessors; + private Consumer testConsumer; + private List testGroup; + private List beforeActions; + private List afterActions; + private Set excludedTests; + private Set includedTests; + private String spaceSubstitute; + private boolean dryRun; + + private final static Map> 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-"; +}