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