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