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.Array; 27 import java.lang.reflect.InvocationTargetException; 28 import java.lang.reflect.Method; 29 import java.lang.reflect.Modifier; 30 import java.util.*; 31 import java.util.concurrent.atomic.AtomicInteger; 32 import java.util.function.Consumer; 33 import java.util.function.Function; 34 import java.util.function.UnaryOperator; 35 import java.util.stream.Collectors; 36 import java.util.stream.Stream; 37 import jdk.jpackage.test.Annotations.AfterEach; 38 import jdk.jpackage.test.Annotations.BeforeEach; 39 import jdk.jpackage.test.Annotations.Parameter; 40 import jdk.jpackage.test.Annotations.ParameterGroup; 41 import jdk.jpackage.test.Annotations.Parameters; 42 import jdk.jpackage.test.Annotations.Test; 43 import jdk.jpackage.test.Functional.ThrowingConsumer; 44 import jdk.jpackage.test.Functional.ThrowingFunction; 45 46 final class TestBuilder implements AutoCloseable { 47 48 @Override 49 public void close() throws Exception { 50 flushTestGroup(); 51 } 52 53 TestBuilder(Consumer<TestInstance> testConsumer) { 54 argProcessors = Map.of( 55 CMDLINE_ARG_PREFIX + "after-run", 56 arg -> getJavaMethodsFromArg(arg).map( 57 this::wrap).forEachOrdered(afterActions::add), 58 59 CMDLINE_ARG_PREFIX + "before-run", 60 arg -> getJavaMethodsFromArg(arg).map( 61 this::wrap).forEachOrdered(beforeActions::add), 62 63 CMDLINE_ARG_PREFIX + "run", 64 arg -> addTestGroup(getJavaMethodsFromArg(arg).map( 65 ThrowingFunction.toFunction( 66 TestBuilder::toMethodCalls)).flatMap(s -> s).collect( 67 Collectors.toList())), 68 69 CMDLINE_ARG_PREFIX + "exclude", 70 arg -> (excludedTests = Optional.ofNullable( 71 excludedTests).orElse(new HashSet<String>())).add(arg), 72 73 CMDLINE_ARG_PREFIX + "include", 74 arg -> (includedTests = Optional.ofNullable( 75 includedTests).orElse(new HashSet<String>())).add(arg), 76 77 CMDLINE_ARG_PREFIX + "space-subst", 78 arg -> spaceSubstitute = arg, 79 80 CMDLINE_ARG_PREFIX + "group", 81 arg -> flushTestGroup(), 82 83 CMDLINE_ARG_PREFIX + "dry-run", 84 arg -> dryRun = true 85 ); 86 this.testConsumer = testConsumer; 87 clear(); 88 } 89 90 void processCmdLineArg(String arg) throws Throwable { 91 int separatorIdx = arg.indexOf('='); 92 final String argName; 93 final String argValue; 94 if (separatorIdx != -1) { 95 argName = arg.substring(0, separatorIdx); 96 argValue = arg.substring(separatorIdx + 1); 97 } else { 98 argName = arg; 99 argValue = null; 100 } 101 try { 102 ThrowingConsumer<String> argProcessor = argProcessors.get(argName); 103 if (argProcessor == null) { 104 throw new ParseException("Unrecognized"); 105 } 106 argProcessor.accept(argValue); 107 } catch (ParseException ex) { 108 ex.setContext(arg); 109 throw ex; 110 } 111 } 112 113 private void addTestGroup(List<MethodCall> newTestGroup) { 114 if (testGroup != null) { 115 testGroup.addAll(newTestGroup); 116 } else { 117 testGroup = newTestGroup; 118 } 119 } 120 121 private static Stream<MethodCall> filterTests(Stream<MethodCall> tests, 122 Set<String> filters, UnaryOperator<Boolean> pred, String logMsg) { 123 if (filters == null) { 124 return tests; 125 } 126 127 // Log all matches before returning from the function 128 return tests.filter(test -> { 129 String testDescription = test.createDescription().testFullName(); 130 boolean match = filters.stream().anyMatch( 131 v -> testDescription.contains(v)); 132 if (match) { 133 trace(String.format(logMsg + ": %s", testDescription)); 134 } 135 return pred.apply(match); 136 }).collect(Collectors.toList()).stream(); 137 } 138 139 private Stream<MethodCall> filterTestGroup() { 140 Objects.requireNonNull(testGroup); 141 142 UnaryOperator<Set<String>> restoreSpaces = filters -> { 143 if (spaceSubstitute == null || filters == null) { 144 return filters; 145 } 146 return filters.stream().map( 147 filter -> filter.replace(spaceSubstitute, " ")).collect( 148 Collectors.toSet()); 149 }; 150 151 if (includedTests != null) { 152 return filterTests(testGroup.stream(), restoreSpaces.apply( 153 includedTests), x -> x, "Include"); 154 } 155 156 return filterTests(testGroup.stream(), 157 restoreSpaces.apply(excludedTests), x -> !x, "Exclude"); 158 } 159 160 private void flushTestGroup() { 161 if (testGroup != null) { 162 filterTestGroup().forEach(testBody -> createTestInstance(testBody)); 163 clear(); 164 } 165 } 166 167 private void createTestInstance(MethodCall testBody) { 168 final List<ThrowingConsumer> curBeforeActions; 169 final List<ThrowingConsumer> curAfterActions; 170 171 Method testMethod = testBody.getMethod(); 172 if (Stream.of(BeforeEach.class, AfterEach.class).anyMatch( 173 type -> testMethod.isAnnotationPresent(type))) { 174 curBeforeActions = beforeActions; 175 curAfterActions = afterActions; 176 } else { 177 curBeforeActions = new ArrayList<>(beforeActions); 178 curAfterActions = new ArrayList<>(afterActions); 179 180 selectFrameMethods(testMethod.getDeclaringClass(), BeforeEach.class).map( 181 this::wrap).forEachOrdered(curBeforeActions::add); 182 selectFrameMethods(testMethod.getDeclaringClass(), AfterEach.class).map( 183 this::wrap).forEachOrdered(curAfterActions::add); 184 } 185 186 TestInstance test = new TestInstance(testBody, curBeforeActions, 187 curAfterActions, dryRun); 188 if (includedTests == null) { 189 trace(String.format("Create: %s", test.fullName())); 190 } 191 testConsumer.accept(test); 192 } 193 194 private void clear() { 195 beforeActions = new ArrayList<>(); 196 afterActions = new ArrayList<>(); 197 excludedTests = null; 198 includedTests = null; 199 spaceSubstitute = null; 200 testGroup = null; 201 } 202 203 private static Class probeClass(String name) { 204 try { 205 return Class.forName(name); 206 } catch (ClassNotFoundException ex) { 207 return null; 208 } 209 } 210 211 private static Stream<Method> selectFrameMethods(Class type, Class annotationType) { 212 return Stream.of(type.getMethods()) 213 .filter(m -> m.getParameterCount() == 0) 214 .filter(m -> !m.isAnnotationPresent(Test.class)) 215 .filter(m -> m.isAnnotationPresent(annotationType)) 216 .sorted((a, b) -> a.getName().compareTo(b.getName())); 217 } 218 219 private static Stream<String> cmdLineArgValueToMethodNames(String v) { 220 List<String> result = new ArrayList<>(); 221 String defaultClassName = null; 222 for (String token : v.split(",")) { 223 Class testSet = probeClass(token); 224 if (testSet != null) { 225 // Test set class specified. Pull in all public methods 226 // from the class with @Test annotation removing name duplicates. 227 // Overloads will be handled at the next phase of processing. 228 defaultClassName = token; 229 Stream.of(testSet.getMethods()).filter( 230 m -> m.isAnnotationPresent(Test.class)).map( 231 Method::getName).distinct().forEach( 232 name -> result.add(String.join(".", token, name))); 233 234 continue; 235 } 236 237 final String qualifiedMethodName; 238 final int lastDotIdx = token.lastIndexOf('.'); 239 if (lastDotIdx != -1) { 240 qualifiedMethodName = token; 241 defaultClassName = token.substring(0, lastDotIdx); 242 } else if (defaultClassName == null) { 243 throw new ParseException("Default class name not found in"); 244 } else { 245 qualifiedMethodName = String.join(".", defaultClassName, token); 246 } 247 result.add(qualifiedMethodName); 248 } 249 return result.stream(); 250 } 251 252 private static boolean filterMethod(String expectedMethodName, Method method) { 253 if (!method.getName().equals(expectedMethodName)) { 254 return false; 255 } 256 switch (method.getParameterCount()) { 257 case 0: 258 return !isParametrized(method); 259 case 1: 260 return isParametrized(method); 261 } 262 return false; 263 } 264 265 private static boolean isParametrized(Method method) { 266 return method.isAnnotationPresent(ParameterGroup.class) || method.isAnnotationPresent( 267 Parameter.class); 268 } 269 270 private static List<Method> getJavaMethodFromString( 271 String qualifiedMethodName) { 272 int lastDotIdx = qualifiedMethodName.lastIndexOf('.'); 273 if (lastDotIdx == -1) { 274 throw new ParseException("Class name not found in"); 275 } 276 String className = qualifiedMethodName.substring(0, lastDotIdx); 277 String methodName = qualifiedMethodName.substring(lastDotIdx + 1); 278 Class methodClass; 279 try { 280 methodClass = Class.forName(className); 281 } catch (ClassNotFoundException ex) { 282 throw new ParseException(String.format("Class [%s] not found;", 283 className)); 284 } 285 // Get the list of all public methods as need to deal with overloads. 286 List<Method> methods = Stream.of(methodClass.getMethods()).filter( 287 (m) -> filterMethod(methodName, m)).collect(Collectors.toList()); 288 if (methods.isEmpty()) { 289 new ParseException(String.format( 290 "Method [%s] not found in [%s] class;", 291 methodName, className)); 292 } 293 294 trace(String.format("%s -> %s", qualifiedMethodName, methods)); 295 return methods; 296 } 297 298 private static Stream<Method> getJavaMethodsFromArg(String argValue) { 299 return cmdLineArgValueToMethodNames(argValue).map( 300 ThrowingFunction.toFunction( 301 TestBuilder::getJavaMethodFromString)).flatMap( 302 List::stream).sequential(); 303 } 304 305 private static Parameter[] getMethodParameters(Method method) { 306 if (method.isAnnotationPresent(ParameterGroup.class)) { 307 return ((ParameterGroup) method.getAnnotation(ParameterGroup.class)).value(); 308 } 309 310 if (method.isAnnotationPresent(Parameter.class)) { 311 return new Parameter[]{(Parameter) method.getAnnotation( 312 Parameter.class)}; 313 } 314 315 // Unexpected 316 return null; 317 } 318 319 private static Stream<Object[]> toCtorArgs(Method method) throws 320 IllegalAccessException, InvocationTargetException { 321 Class type = method.getDeclaringClass(); 322 List<Method> paremetersProviders = Stream.of(type.getMethods()) 323 .filter(m -> m.getParameterCount() == 0) 324 .filter(m -> (m.getModifiers() & Modifier.STATIC) != 0) 325 .filter(m -> m.isAnnotationPresent(Parameters.class)) 326 .sorted() 327 .collect(Collectors.toList()); 328 if (paremetersProviders.isEmpty()) { 329 // Single instance using the default constructor. 330 return Stream.ofNullable(MethodCall.DEFAULT_CTOR_ARGS); 331 } 332 333 // Pick the first method from the list. 334 Method paremetersProvider = paremetersProviders.iterator().next(); 335 if (paremetersProviders.size() > 1) { 336 trace(String.format( 337 "Found %d public static methods without arguments with %s annotation. Will use %s", 338 paremetersProviders.size(), Parameters.class, 339 paremetersProvider)); 340 paremetersProviders.stream().map(Method::toString).forEach( 341 TestBuilder::trace); 342 } 343 344 // Construct collection of arguments for test class instances. 345 return ((Collection) paremetersProvider.invoke(null)).stream(); 346 } 347 348 private static Stream<MethodCall> toMethodCalls(Method method) throws 349 IllegalAccessException, InvocationTargetException { 350 return toCtorArgs(method).map(v -> toMethodCalls(v, method)).flatMap( 351 s -> s).peek(methodCall -> { 352 // Make sure required constructor is accessible if the one is needed. 353 // Need to probe all methods as some of them might be static 354 // and some class members. 355 // Only class members require ctors. 356 try { 357 methodCall.checkRequiredConstructor(); 358 } catch (NoSuchMethodException ex) { 359 throw new ParseException(ex.getMessage() + "."); 360 } 361 }); 362 } 363 364 private static Stream<MethodCall> toMethodCalls(Object[] ctorArgs, Method method) { 365 if (!isParametrized(method)) { 366 return Stream.of(new MethodCall(ctorArgs, method)); 367 } 368 Parameter[] annotations = getMethodParameters(method); 369 if (annotations.length == 0) { 370 return Stream.of(new MethodCall(ctorArgs, method)); 371 } 372 return Stream.of(annotations).map((a) -> { 373 Class paramType = method.getParameterTypes()[0]; 374 final Object annotationValue; 375 if (!paramType.isArray()) { 376 annotationValue = fromString(a.value()[0], paramType); 377 } else { 378 Class paramComponentType = paramType.getComponentType(); 379 annotationValue = Array.newInstance(paramComponentType, a.value().length); 380 var idx = new AtomicInteger(-1); 381 Stream.of(a.value()).map(v -> fromString(v, paramComponentType)).sequential().forEach( 382 v -> Array.set(annotationValue, idx.incrementAndGet(), v)); 383 } 384 return new MethodCall(ctorArgs, method, annotationValue); 385 }); 386 } 387 388 private static Object fromString(String value, Class toType) { 389 Function<String, Object> converter = conv.get(toType); 390 if (converter == null) { 391 throw new RuntimeException(String.format( 392 "Failed to find a conversion of [%s] string to %s type", 393 value, toType)); 394 } 395 return converter.apply(value); 396 } 397 398 // Wraps Method.invike() into ThrowingRunnable.run() 399 private ThrowingConsumer wrap(Method method) { 400 return (test) -> { 401 Class methodClass = method.getDeclaringClass(); 402 String methodName = String.join(".", methodClass.getName(), 403 method.getName()); 404 TKit.log(String.format("[ CALL ] %s()", methodName)); 405 if (!dryRun) { 406 if (methodClass.isInstance(test)) { 407 method.invoke(test); 408 } else { 409 method.invoke(null); 410 } 411 } 412 }; 413 } 414 415 private static class ParseException extends IllegalArgumentException { 416 417 ParseException(String msg) { 418 super(msg); 419 } 420 421 void setContext(String badCmdLineArg) { 422 this.badCmdLineArg = badCmdLineArg; 423 } 424 425 @Override 426 public String getMessage() { 427 String msg = super.getMessage(); 428 if (badCmdLineArg != null) { 429 msg = String.format("%s parameter=[%s]", msg, badCmdLineArg); 430 } 431 return msg; 432 } 433 private String badCmdLineArg; 434 } 435 436 static void trace(String msg) { 437 if (TKit.VERBOSE_TEST_SETUP) { 438 TKit.log(msg); 439 } 440 } 441 442 private final Map<String, ThrowingConsumer<String>> argProcessors; 443 private Consumer<TestInstance> testConsumer; 444 private List<MethodCall> testGroup; 445 private List<ThrowingConsumer> beforeActions; 446 private List<ThrowingConsumer> afterActions; 447 private Set<String> excludedTests; 448 private Set<String> includedTests; 449 private String spaceSubstitute; 450 private boolean dryRun; 451 452 private final static Map<Class, Function<String, Object>> conv = Map.of( 453 boolean.class, Boolean::valueOf, 454 Boolean.class, Boolean::valueOf, 455 int.class, Integer::valueOf, 456 Integer.class, Integer::valueOf, 457 long.class, Long::valueOf, 458 Long.class, Long::valueOf, 459 String.class, String::valueOf); 460 461 final static String CMDLINE_ARG_PREFIX = "--jpt-"; 462 }