|
1 /* |
|
2 * Copyright (c) 2016, 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 import javax.tools.Diagnostic; |
|
25 import javax.tools.DiagnosticListener; |
|
26 import javax.tools.FileObject; |
|
27 import javax.tools.ForwardingJavaFileManager; |
|
28 import javax.tools.JavaCompiler; |
|
29 import javax.tools.JavaFileObject; |
|
30 import javax.tools.SimpleJavaFileObject; |
|
31 import javax.tools.StandardJavaFileManager; |
|
32 import javax.tools.StandardLocation; |
|
33 import javax.tools.ToolProvider; |
|
34 import java.io.BufferedReader; |
|
35 import java.io.ByteArrayOutputStream; |
|
36 import java.io.IOException; |
|
37 import java.io.InputStreamReader; |
|
38 import java.io.OutputStream; |
|
39 import java.io.UncheckedIOException; |
|
40 import java.lang.reflect.Method; |
|
41 import java.net.URI; |
|
42 import java.nio.charset.Charset; |
|
43 import java.util.ArrayList; |
|
44 import java.util.HashMap; |
|
45 import java.util.List; |
|
46 import java.util.Locale; |
|
47 import java.util.Map; |
|
48 import java.util.regex.Pattern; |
|
49 import java.util.stream.Collectors; |
|
50 import java.util.stream.IntStream; |
|
51 import java.util.stream.Stream; |
|
52 |
|
53 import static java.util.stream.Collectors.joining; |
|
54 import static java.util.stream.Collectors.toMap; |
|
55 |
|
56 /* |
|
57 * @test |
|
58 * @bug 8062389 |
|
59 * @summary Nearly exhaustive test of Class.getMethod() and Class.getMethods() |
|
60 * @run main PublicMethodsTest |
|
61 */ |
|
62 public class PublicMethodsTest { |
|
63 |
|
64 public static void main(String[] args) { |
|
65 Case c = new Case1(); |
|
66 |
|
67 int[] diffs = new int[1]; |
|
68 try (Stream<Map.Entry<int[], Map<String, String>>> |
|
69 expected = expectedResults(c)) { |
|
70 diffResults(c, expected) |
|
71 .forEach(diff -> { |
|
72 System.out.println(diff); |
|
73 diffs[0]++; |
|
74 }); |
|
75 } |
|
76 |
|
77 if (diffs[0] > 0) { |
|
78 throw new RuntimeException( |
|
79 "There were " + diffs[0] + " differences."); |
|
80 } |
|
81 } |
|
82 |
|
83 // use this to generate .results file for particular case |
|
84 public static class Generate { |
|
85 public static void main(String[] args) { |
|
86 Case c = new Case1(); |
|
87 dumpResults(generateResults(c)) |
|
88 .forEach(System.out::println); |
|
89 } |
|
90 } |
|
91 |
|
92 interface Case { |
|
93 Pattern PLACEHOLDER_PATTERN = Pattern.compile("\\$\\{(.+?)}"); |
|
94 |
|
95 // possible variants of interface method |
|
96 List<String> INTERFACE_METHODS = List.of( |
|
97 "", "void m();", "default void m() {}", "static void m() {}" |
|
98 ); |
|
99 |
|
100 // possible variants of class method |
|
101 List<String> CLASS_METHODS = List.of( |
|
102 "", "public abstract void m();", |
|
103 "public void m() {}", "public static void m() {}" |
|
104 ); |
|
105 |
|
106 // template with placeholders parsed with PLACEHOLDER_PATTERN |
|
107 String template(); |
|
108 |
|
109 // map of replacementKey (== PLACEHOLDER_PATTERN captured group #1) -> |
|
110 // list of possible replacements |
|
111 Map<String, List<String>> replacements(); |
|
112 |
|
113 // ordered list of replacement keys |
|
114 List<String> replacementKeys(); |
|
115 |
|
116 // names of types occurring in the template |
|
117 List<String> classNames(); |
|
118 } |
|
119 |
|
120 static class Case1 implements Case { |
|
121 |
|
122 private static final String TEMPLATE = Stream.of( |
|
123 "interface I { ${I} }", |
|
124 "interface J { ${J} }", |
|
125 "interface K extends I, J { ${K} }", |
|
126 "abstract class C { ${C} }", |
|
127 "abstract class D extends C implements I { ${D} }", |
|
128 "abstract class E extends D implements J, K { ${E} }" |
|
129 ).collect(joining("\n")); |
|
130 |
|
131 private static final Map<String, List<String>> REPLACEMENTS = Map.of( |
|
132 "I", INTERFACE_METHODS, |
|
133 "J", INTERFACE_METHODS, |
|
134 "K", INTERFACE_METHODS, |
|
135 "C", CLASS_METHODS, |
|
136 "D", CLASS_METHODS, |
|
137 "E", CLASS_METHODS |
|
138 ); |
|
139 |
|
140 private static final List<String> REPLACEMENT_KEYS = REPLACEMENTS |
|
141 .keySet().stream().sorted().collect(Collectors.toList()); |
|
142 |
|
143 @Override |
|
144 public String template() { |
|
145 return TEMPLATE; |
|
146 } |
|
147 |
|
148 @Override |
|
149 public Map<String, List<String>> replacements() { |
|
150 return REPLACEMENTS; |
|
151 } |
|
152 |
|
153 @Override |
|
154 public List<String> replacementKeys() { |
|
155 return REPLACEMENT_KEYS; |
|
156 } |
|
157 |
|
158 @Override |
|
159 public List<String> classNames() { |
|
160 // just by accident, names of classes are equal to replacement keys |
|
161 // (this need not be the case in general) |
|
162 return REPLACEMENT_KEYS; |
|
163 } |
|
164 } |
|
165 |
|
166 // generate all combinations as a tuple of indexes into lists of |
|
167 // replacements. The index of the element in int[] tuple represents the index |
|
168 // of the key in replacementKeys() list. The value of the element in int[] tuple |
|
169 // represents the index of the replacement string in list of strings in the |
|
170 // value of the entry of replacements() map with the corresponding key. |
|
171 static Stream<int[]> combinations(Case c) { |
|
172 int[] sizes = c.replacementKeys().stream() |
|
173 .mapToInt(key -> c.replacements().get(key).size()) |
|
174 .toArray(); |
|
175 |
|
176 return Stream.iterate( |
|
177 new int[sizes.length], |
|
178 state -> state != null, |
|
179 state -> { |
|
180 int[] newState = state.clone(); |
|
181 for (int i = 0; i < state.length; i++) { |
|
182 if (++newState[i] < sizes[i]) { |
|
183 return newState; |
|
184 } |
|
185 newState[i] = 0; |
|
186 } |
|
187 // wrapped-around |
|
188 return null; |
|
189 } |
|
190 ); |
|
191 } |
|
192 |
|
193 // given the combination of indexes, return the expanded template |
|
194 static String expandTemplate(Case c, int[] combination) { |
|
195 |
|
196 // 1st create a map: key -> replacement string |
|
197 Map<String, String> map = new HashMap<>(combination.length * 4 / 3 + 1); |
|
198 for (int i = 0; i < combination.length; i++) { |
|
199 String key = c.replacementKeys().get(i); |
|
200 String repl = c.replacements().get(key).get(combination[i]); |
|
201 map.put(key, repl); |
|
202 } |
|
203 |
|
204 return Case.PLACEHOLDER_PATTERN |
|
205 .matcher(c.template()) |
|
206 .replaceAll(match -> map.get(match.group(1))); |
|
207 } |
|
208 |
|
209 /** |
|
210 * compile expanded template into a ClassLoader that sees compiled classes |
|
211 */ |
|
212 static ClassLoader compile(String source) throws CompileException { |
|
213 JavaCompiler javac = ToolProvider.getSystemJavaCompiler(); |
|
214 if (javac == null) { |
|
215 throw new AssertionError("No Java compiler tool found."); |
|
216 } |
|
217 |
|
218 ErrorsCollector errorsCollector = new ErrorsCollector(); |
|
219 StandardJavaFileManager standardJavaFileManager = |
|
220 javac.getStandardFileManager(errorsCollector, Locale.ROOT, |
|
221 Charset.forName("UTF-8")); |
|
222 TestFileManager testFileManager = new TestFileManager( |
|
223 standardJavaFileManager, source); |
|
224 |
|
225 JavaCompiler.CompilationTask javacTask; |
|
226 try { |
|
227 javacTask = javac.getTask( |
|
228 null, // use System.err |
|
229 testFileManager, |
|
230 errorsCollector, |
|
231 null, |
|
232 null, |
|
233 List.of(testFileManager.getJavaFileForInput( |
|
234 StandardLocation.SOURCE_PATH, |
|
235 TestFileManager.TEST_CLASS_NAME, |
|
236 JavaFileObject.Kind.SOURCE)) |
|
237 ); |
|
238 } catch (IOException e) { |
|
239 throw new UncheckedIOException(e); |
|
240 } |
|
241 |
|
242 javacTask.call(); |
|
243 |
|
244 if (errorsCollector.hasError()) { |
|
245 throw new CompileException(errorsCollector.getErrors()); |
|
246 } |
|
247 |
|
248 return new TestClassLoader(ClassLoader.getSystemClassLoader(), |
|
249 testFileManager); |
|
250 } |
|
251 |
|
252 static class CompileException extends Exception { |
|
253 CompileException(List<Diagnostic<?>> diagnostics) { |
|
254 super(diagnostics.stream() |
|
255 .map(diag -> diag.toString()) |
|
256 .collect(Collectors.joining("\n"))); |
|
257 } |
|
258 } |
|
259 |
|
260 static class TestFileManager |
|
261 extends ForwardingJavaFileManager<StandardJavaFileManager> { |
|
262 static final String TEST_CLASS_NAME = "Test"; |
|
263 |
|
264 private final String testSource; |
|
265 private final Map<String, ClassFileObject> classes = new HashMap<>(); |
|
266 |
|
267 TestFileManager(StandardJavaFileManager fileManager, String source) { |
|
268 super(fileManager); |
|
269 testSource = "public class " + TEST_CLASS_NAME + " {}\n" + |
|
270 source; // the rest of classes are package-private |
|
271 } |
|
272 |
|
273 @Override |
|
274 public JavaFileObject getJavaFileForInput(Location location, |
|
275 String className, |
|
276 JavaFileObject.Kind kind) |
|
277 throws IOException { |
|
278 if (location == StandardLocation.SOURCE_PATH && |
|
279 kind == JavaFileObject.Kind.SOURCE && |
|
280 TEST_CLASS_NAME.equals(className)) { |
|
281 return new SourceFileObject(className, testSource); |
|
282 } |
|
283 return super.getJavaFileForInput(location, className, kind); |
|
284 } |
|
285 |
|
286 private static class SourceFileObject extends SimpleJavaFileObject { |
|
287 private final String source; |
|
288 |
|
289 SourceFileObject(String className, String source) { |
|
290 super( |
|
291 URI.create("memory:/src/" + |
|
292 className.replace('.', '/') + ".java"), |
|
293 Kind.SOURCE |
|
294 ); |
|
295 this.source = source; |
|
296 } |
|
297 |
|
298 @Override |
|
299 public CharSequence getCharContent(boolean ignoreEncodingErrors) { |
|
300 return source; |
|
301 } |
|
302 } |
|
303 |
|
304 @Override |
|
305 public JavaFileObject getJavaFileForOutput(Location location, |
|
306 String className, |
|
307 JavaFileObject.Kind kind, |
|
308 FileObject sibling) |
|
309 throws IOException { |
|
310 if (kind == JavaFileObject.Kind.CLASS) { |
|
311 ClassFileObject cfo = new ClassFileObject(className); |
|
312 classes.put(className, cfo); |
|
313 return cfo; |
|
314 } |
|
315 return super.getJavaFileForOutput(location, className, kind, sibling); |
|
316 } |
|
317 |
|
318 private static class ClassFileObject extends SimpleJavaFileObject { |
|
319 final String className; |
|
320 ByteArrayOutputStream byteArrayOutputStream; |
|
321 |
|
322 ClassFileObject(String className) { |
|
323 super( |
|
324 URI.create("memory:/out/" + |
|
325 className.replace('.', '/') + ".class"), |
|
326 Kind.CLASS |
|
327 ); |
|
328 this.className = className; |
|
329 } |
|
330 |
|
331 @Override |
|
332 public OutputStream openOutputStream() throws IOException { |
|
333 return byteArrayOutputStream = new ByteArrayOutputStream(); |
|
334 } |
|
335 |
|
336 byte[] getBytes() { |
|
337 if (byteArrayOutputStream == null) { |
|
338 throw new IllegalStateException( |
|
339 "No class file written for class: " + className); |
|
340 } |
|
341 return byteArrayOutputStream.toByteArray(); |
|
342 } |
|
343 } |
|
344 |
|
345 byte[] getClassBytes(String className) { |
|
346 ClassFileObject cfo = classes.get(className); |
|
347 return (cfo == null) ? null : cfo.getBytes(); |
|
348 } |
|
349 } |
|
350 |
|
351 static class ErrorsCollector implements DiagnosticListener<JavaFileObject> { |
|
352 private final List<Diagnostic<?>> errors = new ArrayList<>(); |
|
353 |
|
354 @Override |
|
355 public void report(Diagnostic<? extends JavaFileObject> diagnostic) { |
|
356 if (diagnostic.getKind() == Diagnostic.Kind.ERROR) { |
|
357 errors.add(diagnostic); |
|
358 } |
|
359 } |
|
360 |
|
361 boolean hasError() { |
|
362 return !errors.isEmpty(); |
|
363 } |
|
364 |
|
365 List<Diagnostic<?>> getErrors() { |
|
366 return errors; |
|
367 } |
|
368 } |
|
369 |
|
370 static class TestClassLoader extends ClassLoader { |
|
371 private final TestFileManager fileManager; |
|
372 |
|
373 public TestClassLoader(ClassLoader parent, TestFileManager fileManager) { |
|
374 super(parent); |
|
375 this.fileManager = fileManager; |
|
376 } |
|
377 |
|
378 @Override |
|
379 protected Class<?> findClass(String name) throws ClassNotFoundException { |
|
380 byte[] classBytes = fileManager.getClassBytes(name); |
|
381 if (classBytes == null) { |
|
382 throw new ClassNotFoundException(name); |
|
383 } |
|
384 return defineClass(name, classBytes, 0, classBytes.length); |
|
385 } |
|
386 } |
|
387 |
|
388 static Map<String, String> generateResult(Case c, ClassLoader cl) { |
|
389 return |
|
390 c.classNames() |
|
391 .stream() |
|
392 .map(cn -> { |
|
393 try { |
|
394 return Class.forName(cn, false, cl); |
|
395 } catch (ClassNotFoundException e) { |
|
396 throw new RuntimeException("Class not found: " + cn, e); |
|
397 } |
|
398 }) |
|
399 .flatMap(clazz -> Stream.of( |
|
400 Map.entry(clazz.getName() + ".gM", generateGetMethodResult(clazz)), |
|
401 Map.entry(clazz.getName() + ".gMs", generateGetMethodsResult(clazz)) |
|
402 )) |
|
403 .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); |
|
404 } |
|
405 |
|
406 static String generateGetMethodResult(Class<?> clazz) { |
|
407 try { |
|
408 Method m = clazz.getMethod("m"); |
|
409 return m.getDeclaringClass().getName() + "." + m.getName(); |
|
410 } catch (NoSuchMethodException e) { |
|
411 return "-"; |
|
412 } |
|
413 } |
|
414 |
|
415 static String generateGetMethodsResult(Class<?> clazz) { |
|
416 return Stream.of(clazz.getMethods()) |
|
417 .filter(m -> m.getDeclaringClass() != Object.class) |
|
418 .map(m -> m.getDeclaringClass().getName() |
|
419 + "." + m.getName()) |
|
420 .collect(Collectors.joining(", ", "[", "]")); |
|
421 } |
|
422 |
|
423 static Stream<Map.Entry<int[], Map<String, String>>> generateResults(Case c) { |
|
424 return combinations(c) |
|
425 .flatMap(comb -> { |
|
426 String src = expandTemplate(c, comb); |
|
427 ClassLoader cl; |
|
428 try { |
|
429 cl = compile(src); |
|
430 } catch (CompileException e) { |
|
431 // ignore uncompilable combinations |
|
432 return Stream.empty(); |
|
433 } |
|
434 // compilation was successful -> generate result |
|
435 return Stream.of(Map.entry( |
|
436 comb, |
|
437 generateResult(c, cl) |
|
438 )); |
|
439 }); |
|
440 } |
|
441 |
|
442 static Stream<Map.Entry<int[], Map<String, String>>> expectedResults(Case c) { |
|
443 try { |
|
444 BufferedReader r = new BufferedReader(new InputStreamReader( |
|
445 c.getClass().getResourceAsStream( |
|
446 c.getClass().getSimpleName() + ".results"), |
|
447 "UTF-8" |
|
448 )); |
|
449 |
|
450 return parseResults(r.lines()) |
|
451 .onClose(() -> { |
|
452 try { |
|
453 r.close(); |
|
454 } catch (IOException ioe) { |
|
455 throw new UncheckedIOException(ioe); |
|
456 } |
|
457 }); |
|
458 } catch (IOException e) { |
|
459 throw new UncheckedIOException(e); |
|
460 } |
|
461 } |
|
462 |
|
463 static Stream<Map.Entry<int[], Map<String, String>>> parseResults( |
|
464 Stream<String> lines |
|
465 ) { |
|
466 return lines |
|
467 .map(l -> l.split(Pattern.quote("#"))) |
|
468 .map(lkv -> Map.entry( |
|
469 Stream.of(lkv[0].split(Pattern.quote(","))) |
|
470 .mapToInt(Integer::parseInt) |
|
471 .toArray(), |
|
472 Stream.of(lkv[1].split(Pattern.quote("|"))) |
|
473 .map(e -> e.split(Pattern.quote("="))) |
|
474 .collect(toMap(ekv -> ekv[0], ekv -> ekv[1])) |
|
475 )); |
|
476 } |
|
477 |
|
478 static Stream<String> dumpResults( |
|
479 Stream<Map.Entry<int[], Map<String, String>>> results |
|
480 ) { |
|
481 return results |
|
482 .map(le -> |
|
483 IntStream.of(le.getKey()) |
|
484 .mapToObj(String::valueOf) |
|
485 .collect(joining(",")) |
|
486 + "#" + |
|
487 le.getValue().entrySet().stream() |
|
488 .map(e -> e.getKey() + "=" + e.getValue()) |
|
489 .collect(joining("|")) |
|
490 ); |
|
491 } |
|
492 |
|
493 static Stream<String> diffResults( |
|
494 Case c, |
|
495 Stream<Map.Entry<int[], Map<String, String>>> expectedResults |
|
496 ) { |
|
497 return expectedResults |
|
498 .flatMap(exp -> { |
|
499 int[] comb = exp.getKey(); |
|
500 Map<String, String> expected = exp.getValue(); |
|
501 |
|
502 String src = expandTemplate(c, comb); |
|
503 ClassLoader cl; |
|
504 try { |
|
505 cl = compile(src); |
|
506 } catch (CompileException ce) { |
|
507 return Stream.of(src + "\n" + |
|
508 "got compilation error: " + ce); |
|
509 } |
|
510 |
|
511 Map<String, String> actual = generateResult(c, cl); |
|
512 if (actual.equals(expected)) { |
|
513 return Stream.empty(); |
|
514 } else { |
|
515 Map<String, String> diff = new HashMap<>(expected); |
|
516 diff.entrySet().removeAll(actual.entrySet()); |
|
517 return Stream.of( |
|
518 diff.entrySet() |
|
519 .stream() |
|
520 .map(e -> "expected: " + e.getKey() + ": " + |
|
521 e.getValue() + "\n" + |
|
522 " actual: " + e.getKey() + ": " + |
|
523 actual.get(e.getKey()) + "\n") |
|
524 .collect(joining("\n", src + "\n\n", "\n")) |
|
525 ); |
|
526 } |
|
527 }); |
|
528 } |
|
529 } |