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