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