21 * questions. |
21 * questions. |
22 */ |
22 */ |
23 |
23 |
24 package jdk.jpackage.test; |
24 package jdk.jpackage.test; |
25 |
25 |
26 import java.lang.reflect.Constructor; |
26 import java.lang.reflect.Array; |
27 import java.lang.reflect.InvocationTargetException; |
27 import java.lang.reflect.InvocationTargetException; |
28 import java.lang.reflect.Method; |
28 import java.lang.reflect.Method; |
29 import java.util.ArrayList; |
29 import java.lang.reflect.Modifier; |
30 import java.util.List; |
30 import java.util.*; |
31 import java.util.Map; |
31 import java.util.concurrent.atomic.AtomicInteger; |
32 import java.util.function.Consumer; |
32 import java.util.function.Consumer; |
33 import java.util.function.Function; |
33 import java.util.function.Function; |
|
34 import java.util.function.UnaryOperator; |
34 import java.util.stream.Collectors; |
35 import java.util.stream.Collectors; |
35 import java.util.stream.Stream; |
36 import java.util.stream.Stream; |
|
37 import jdk.jpackage.test.Annotations.AfterEach; |
|
38 import jdk.jpackage.test.Annotations.BeforeEach; |
36 import jdk.jpackage.test.Annotations.Parameter; |
39 import jdk.jpackage.test.Annotations.Parameter; |
|
40 import jdk.jpackage.test.Annotations.ParameterGroup; |
37 import jdk.jpackage.test.Annotations.Parameters; |
41 import jdk.jpackage.test.Annotations.Parameters; |
38 import jdk.jpackage.test.Annotations.Test; |
42 import jdk.jpackage.test.Annotations.Test; |
39 import jdk.jpackage.test.Functional.ThrowingConsumer; |
43 import jdk.jpackage.test.Functional.ThrowingConsumer; |
40 import jdk.jpackage.test.Functional.ThrowingFunction; |
44 import jdk.jpackage.test.Functional.ThrowingFunction; |
41 import jdk.jpackage.test.Functional.ThrowingSupplier; |
|
42 |
45 |
43 final class TestBuilder implements AutoCloseable { |
46 final class TestBuilder implements AutoCloseable { |
44 |
47 |
45 @Override |
48 @Override |
46 public void close() throws Exception { |
49 public void close() throws Exception { |
47 flushTestGroup(null); |
50 flushTestGroup(); |
48 } |
51 } |
49 |
52 |
50 TestBuilder(Consumer<TestInstance> testConsumer) { |
53 TestBuilder(Consumer<TestInstance> testConsumer) { |
51 argProcessors = Map.of(CMDLINE_ARG_PREFIX + "after-run", |
54 argProcessors = Map.of( |
52 arg -> getJavaMethodsFromArg(arg).forEach( |
55 CMDLINE_ARG_PREFIX + "after-run", |
53 (method) -> afterActions.add(wrap(method, dryRun))), |
56 arg -> getJavaMethodsFromArg(arg).map( |
|
57 this::wrap).forEachOrdered(afterActions::add), |
|
58 |
54 CMDLINE_ARG_PREFIX + "before-run", |
59 CMDLINE_ARG_PREFIX + "before-run", |
55 arg -> getJavaMethodsFromArg(arg).forEach( |
60 arg -> getJavaMethodsFromArg(arg).map( |
56 (method) -> beforeActions.add(wrap(method, dryRun))), |
61 this::wrap).forEachOrdered(beforeActions::add), |
|
62 |
57 CMDLINE_ARG_PREFIX + "run", |
63 CMDLINE_ARG_PREFIX + "run", |
58 arg -> flushTestGroup(getJavaMethodsFromArg(arg).map( |
64 arg -> addTestGroup(getJavaMethodsFromArg(arg).map( |
59 TestBuilder::toMethodCalls).flatMap(List::stream).collect( |
65 ThrowingFunction.toFunction( |
|
66 TestBuilder::toMethodCalls)).flatMap(s -> s).collect( |
60 Collectors.toList())), |
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 |
61 CMDLINE_ARG_PREFIX + "dry-run", |
83 CMDLINE_ARG_PREFIX + "dry-run", |
62 arg -> dryRun = true); |
84 arg -> dryRun = true |
|
85 ); |
63 this.testConsumer = testConsumer; |
86 this.testConsumer = testConsumer; |
64 clear(); |
87 clear(); |
65 } |
88 } |
66 |
89 |
67 void processCmdLineArg(String arg) throws Throwable { |
90 void processCmdLineArg(String arg) throws Throwable { |
85 ex.setContext(arg); |
108 ex.setContext(arg); |
86 throw ex; |
109 throw ex; |
87 } |
110 } |
88 } |
111 } |
89 |
112 |
90 private void flushTestGroup(List<MethodCall> newTestGroup) { |
113 private void addTestGroup(List<MethodCall> newTestGroup) { |
91 if (testGroup != null) { |
114 if (testGroup != null) { |
92 testGroup.forEach(testBody -> createTestInstance(testBody)); |
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)); |
93 clear(); |
163 clear(); |
94 } |
164 } |
95 testGroup = newTestGroup; |
|
96 } |
165 } |
97 |
166 |
98 private void createTestInstance(MethodCall testBody) { |
167 private void createTestInstance(MethodCall testBody) { |
99 ThrowingFunction<MethodCall, Object> testContructor; |
168 final List<ThrowingConsumer> curBeforeActions; |
100 if (dryRun) { |
169 final List<ThrowingConsumer> curAfterActions; |
101 testContructor = (unused) -> null; |
170 |
102 testBody = DRY_RUN_TEST_BODY; |
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; |
103 } else { |
176 } else { |
104 testContructor = TestBuilder::constructTest; |
177 curBeforeActions = new ArrayList<>(beforeActions); |
105 } |
178 curAfterActions = new ArrayList<>(afterActions); |
106 |
179 |
107 TestInstance test = new TestInstance(testContructor, testBody, |
180 selectFrameMethods(testMethod.getDeclaringClass(), BeforeEach.class).map( |
108 beforeActions, afterActions); |
181 this::wrap).forEachOrdered(curBeforeActions::add); |
109 trace(String.format("[%s] test constructed", test.fullName())); |
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 } |
110 testConsumer.accept(test); |
191 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 } |
192 } |
130 |
193 |
131 private void clear() { |
194 private void clear() { |
132 beforeActions = new ArrayList<>(); |
195 beforeActions = new ArrayList<>(); |
133 afterActions = new ArrayList<>(); |
196 afterActions = new ArrayList<>(); |
|
197 excludedTests = null; |
|
198 includedTests = null; |
|
199 spaceSubstitute = null; |
134 testGroup = null; |
200 testGroup = null; |
135 } |
201 } |
136 |
202 |
137 private static Class probeClass(String name) { |
203 private static Class probeClass(String name) { |
138 try { |
204 try { |
139 return Class.forName(name); |
205 return Class.forName(name); |
140 } catch (ClassNotFoundException ex) { |
206 } catch (ClassNotFoundException ex) { |
141 return null; |
207 return null; |
142 } |
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())); |
143 } |
217 } |
144 |
218 |
145 private static Stream<String> cmdLineArgValueToMethodNames(String v) { |
219 private static Stream<String> cmdLineArgValueToMethodNames(String v) { |
146 List<String> result = new ArrayList<>(); |
220 List<String> result = new ArrayList<>(); |
147 String defaultClassName = null; |
221 String defaultClassName = null; |
213 if (methods.isEmpty()) { |
288 if (methods.isEmpty()) { |
214 new ParseException(String.format( |
289 new ParseException(String.format( |
215 "Method [%s] not found in [%s] class;", |
290 "Method [%s] not found in [%s] class;", |
216 methodName, className)); |
291 methodName, className)); |
217 } |
292 } |
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 |
293 |
232 trace(String.format("%s -> %s", qualifiedMethodName, methods)); |
294 trace(String.format("%s -> %s", qualifiedMethodName, methods)); |
233 return methods; |
295 return methods; |
234 } |
296 } |
235 |
297 |
252 |
314 |
253 // Unexpected |
315 // Unexpected |
254 return null; |
316 return null; |
255 } |
317 } |
256 |
318 |
257 private static List<MethodCall> toMethodCalls(Method method) { |
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) { |
258 if (!isParametrized(method)) { |
365 if (!isParametrized(method)) { |
259 return List.of(new MethodCall(method)); |
366 return Stream.of(new MethodCall(ctorArgs, method)); |
260 } |
367 } |
261 Parameter[] annotations = getParameters(method); |
368 Parameter[] annotations = getMethodParameters(method); |
262 if (annotations.length == 0) { |
369 if (annotations.length == 0) { |
263 return List.of(new MethodCall(method)); |
370 return Stream.of(new MethodCall(ctorArgs, method)); |
264 } |
371 } |
265 return Stream.of(annotations).map((a) -> { |
372 return Stream.of(annotations).map((a) -> { |
266 String annotationValue = a.value(); |
373 Class paramType = method.getParameterTypes()[0]; |
267 Class paramClass = method.getParameterTypes()[0]; |
374 final Object annotationValue; |
268 return new MethodCall(method, |
375 if (!paramType.isArray()) { |
269 fromString(annotationValue, paramClass)); |
376 annotationValue = fromString(a.value()[0], paramType); |
270 }).collect(Collectors.toList()); |
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 }); |
271 } |
386 } |
272 |
387 |
273 private static Object fromString(String value, Class toType) { |
388 private static Object fromString(String value, Class toType) { |
274 Function<String, Object> converter = conv.get(toType); |
389 Function<String, Object> converter = conv.get(toType); |
275 if (converter == null) { |
390 if (converter == null) { |
316 return msg; |
431 return msg; |
317 } |
432 } |
318 private String badCmdLineArg; |
433 private String badCmdLineArg; |
319 } |
434 } |
320 |
435 |
321 private static void trace(String msg) { |
436 static void trace(String msg) { |
322 if (TKit.VERBOSE_TEST_SETUP) { |
437 if (TKit.VERBOSE_TEST_SETUP) { |
323 TKit.log(msg); |
438 TKit.log(msg); |
324 } |
439 } |
325 } |
440 } |
326 |
441 |
327 private final Map<String, ThrowingConsumer<String>> argProcessors; |
442 private final Map<String, ThrowingConsumer<String>> argProcessors; |
328 private Consumer<TestInstance> testConsumer; |
443 private Consumer<TestInstance> testConsumer; |
329 private List<MethodCall> testGroup; |
444 private List<MethodCall> testGroup; |
330 private List<ThrowingConsumer> beforeActions; |
445 private List<ThrowingConsumer> beforeActions; |
331 private List<ThrowingConsumer> afterActions; |
446 private List<ThrowingConsumer> afterActions; |
|
447 private Set<String> excludedTests; |
|
448 private Set<String> includedTests; |
|
449 private String spaceSubstitute; |
332 private boolean dryRun; |
450 private boolean dryRun; |
333 |
451 |
334 private final static Map<Class, Function<String, Object>> conv = Map.of( |
452 private final static Map<Class, Function<String, Object>> conv = Map.of( |
335 boolean.class, Boolean::valueOf, |
453 boolean.class, Boolean::valueOf, |
336 Boolean.class, Boolean::valueOf, |
454 Boolean.class, Boolean::valueOf, |