|
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 } |