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 }