JdepsDependencyClosure.java revision 3170:dc017a37aac5
1/* 2 * Copyright (c) 2015, 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 24import java.io.IOException; 25import java.io.OutputStream; 26import java.io.PrintWriter; 27import java.nio.file.Paths; 28import java.util.ArrayList; 29import java.util.Arrays; 30import java.util.HashMap; 31import java.util.HashSet; 32import java.util.LinkedHashSet; 33import java.util.List; 34import java.util.Locale; 35import java.util.Map; 36import java.util.Set; 37import java.util.function.Supplier; 38import java.util.stream.Collectors; 39import java.util.stream.Stream; 40 41/** 42 * @test 43 * @bug 8080608 44 * @summary Test that jdeps verbose output has a summary line when dependencies 45 * are found within the same archive. For each testcase, compare the 46 * result obtained from jdeps with the expected result. 47 * @modules jdk.jdeps/com.sun.tools.jdeps 48 * @build use.indirect.DontUseUnsafe2 49 * @build use.indirect.UseUnsafeIndirectly 50 * @build use.indirect2.DontUseUnsafe3 51 * @build use.indirect2.UseUnsafeIndirectly2 52 * @build use.unsafe.DontUseUnsafe 53 * @build use.unsafe.UseClassWithUnsafe 54 * @build use.unsafe.UseUnsafeClass 55 * @build use.unsafe.UseUnsafeClass2 56 * @run main JdepsDependencyClosure --test:0 57 * @run main JdepsDependencyClosure --test:1 58 * @run main JdepsDependencyClosure --test:2 59 * @run main JdepsDependencyClosure --test:3 60 */ 61public class JdepsDependencyClosure { 62 63 static boolean VERBOSE = false; 64 static boolean COMPARE_TEXT = true; 65 66 static final String JDEPS_SUMMARY_TEXT_FORMAT = "%s -> %s%n"; 67 static final String JDEPS_VERBOSE_TEXT_FORMAT = " %-50s -> %-50s %s%n"; 68 69 /** 70 * Helper class used to store arguments to pass to 71 * {@code JdepsDependencyClosure.test} as well as expected 72 * results. 73 */ 74 static class TestCaseData { 75 final Map<String, Set<String>> expectedDependencies; 76 final String expectedText; 77 final String[] args; 78 final boolean closure; 79 80 TestCaseData(Map<String, Set<String>> expectedDependencies, 81 String expectedText, 82 boolean closure, 83 String[] args) { 84 this.expectedDependencies = expectedDependencies; 85 this.expectedText = expectedText; 86 this.closure = closure; 87 this.args = args; 88 } 89 90 public void test() { 91 if (expectedDependencies != null) { 92 String format = closure 93 ? "Running (closure): jdeps %s %s %s %s" 94 : "Running: jdeps %s %s %s %s"; 95 System.out.println(String.format(format, (Object[])args)); 96 } 97 JdepsDependencyClosure.test(args, expectedDependencies, expectedText, closure); 98 } 99 100 /** 101 * Make a new test case data to invoke jdeps and test its output. 102 * @param pattern The pattern that will passed through to jdeps -e 103 * This is expected to match only one class. 104 * @param arcPath The archive to analyze. A jar or a class directory. 105 * @param classes For each reported archive dependency couple, the 106 * expected list of classes in the source that will 107 * be reported as having a dependency on the class 108 * in the target that matches the given pattern. 109 * @param dependencies For each archive dependency couple, a singleton list 110 * containing the name of the class in the target that 111 * matches the pattern. It is expected that the pattern 112 * will match only one class in the target. 113 * If the pattern matches several classes the 114 * expected text may no longer match the jdeps output. 115 * @param archives A list of archive dependency couple in the form 116 * {{sourceName1, sourcePath1, targetDescription1, targetPath1} 117 * {sourceName2, sourcePath2, targetDescription2, targetPath2} 118 * ... } 119 * For a JDK module - e.g. java.base, the targetDescription 120 * is usually something like "JDK internal API (java.base)" 121 * and the targetPath is usually the module name "java.base". 122 * @param closure Whether jdeps should be recursively invoked to build 123 * the closure. 124 * @return An instance of TestCaseData containing all the information 125 * needed to perform the jdeps invokation and test its output. 126 */ 127 public static TestCaseData make(String pattern, String arcPath, String[][] classes, 128 String[][] dependencies, String[][] archives, boolean closure) { 129 final String[] args = new String[] { 130 "-e", pattern, "-v", arcPath 131 }; 132 Map<String, Set<String>> expected = new HashMap<>(); 133 String expectedText = ""; 134 for (int i=0; i<classes.length; i++) { 135 final int index = i; 136 expectedText += Stream.of(classes[i]) 137 .map((cn) -> String.format(JDEPS_VERBOSE_TEXT_FORMAT, cn, 138 dependencies[index][0], archives[index][2])) 139 .reduce(String.format(JDEPS_SUMMARY_TEXT_FORMAT, archives[i][0], 140 archives[index][3]), (s1,s2) -> s1.concat(s2)); 141 for (String cn : classes[index]) { 142 expected.putIfAbsent(cn, new HashSet<>()); 143 expected.get(cn).add(dependencies[index][0]); 144 } 145 } 146 return new TestCaseData(expected, expectedText, closure, args); 147 } 148 149 public static TestCaseData valueOf(String[] args) { 150 if (args.length == 1 && args[0].startsWith("--test:")) { 151 // invoked from jtreg. build test case data for selected test. 152 int index = Integer.parseInt(args[0].substring("--test:".length())); 153 if (index >= dataSuppliers.size()) { 154 throw new RuntimeException("No such test case: " + index 155 + " - available testcases are [0.." 156 + (dataSuppliers.size()-1) + "]"); 157 } 158 return dataSuppliers.get(index).get(); 159 } else { 160 // invoked in standalone. just take the given argument 161 // and perform no validation on the output (except that it 162 // must start with a summary line) 163 return new TestCaseData(null, null, true, args); 164 } 165 } 166 167 } 168 169 static TestCaseData makeTestCaseOne() { 170 final String arcPath = System.getProperty("test.classes", "build/classes"); 171 final String arcName = Paths.get(arcPath).getFileName().toString(); 172 final String[][] classes = new String[][] { 173 {"use.indirect2.UseUnsafeIndirectly2", "use.unsafe.UseClassWithUnsafe"}, 174 }; 175 final String[][] dependencies = new String[][] { 176 {"use.unsafe.UseUnsafeClass"}, 177 }; 178 final String[][] archives = new String[][] { 179 {arcName, arcPath, arcName, arcPath}, 180 }; 181 return TestCaseData.make("use.unsafe.UseUnsafeClass", arcPath, classes, 182 dependencies, archives, false); 183 } 184 185 static TestCaseData makeTestCaseTwo() { 186 String arcPath = System.getProperty("test.classes", "build/classes"); 187 String arcName = Paths.get(arcPath).getFileName().toString(); 188 String[][] classes = new String[][] { 189 {"use.unsafe.UseUnsafeClass", "use.unsafe.UseUnsafeClass2"} 190 }; 191 String[][] dependencies = new String[][] { 192 {"sun.misc.Unsafe"} 193 }; 194 String[][] archive = new String[][] { 195 {arcName, arcPath, "JDK internal API (java.base)", "java.base"}, 196 }; 197 return TestCaseData.make("sun.misc.Unsafe", arcPath, classes, 198 dependencies, archive, false); 199 } 200 201 static TestCaseData makeTestCaseThree() { 202 final String arcPath = System.getProperty("test.classes", "build/classes"); 203 final String arcName = Paths.get(arcPath).getFileName().toString(); 204 final String[][] classes = new String[][] { 205 {"use.indirect2.UseUnsafeIndirectly2", "use.unsafe.UseClassWithUnsafe"}, 206 {"use.indirect.UseUnsafeIndirectly"} 207 }; 208 final String[][] dependencies = new String[][] { 209 {"use.unsafe.UseUnsafeClass"}, 210 {"use.unsafe.UseClassWithUnsafe"} 211 }; 212 final String[][] archives = new String[][] { 213 {arcName, arcPath, arcName, arcPath}, 214 {arcName, arcPath, arcName, arcPath} 215 }; 216 return TestCaseData.make("use.unsafe.UseUnsafeClass", arcPath, classes, 217 dependencies, archives, true); 218 } 219 220 221 static TestCaseData makeTestCaseFour() { 222 final String arcPath = System.getProperty("test.classes", "build/classes"); 223 final String arcName = Paths.get(arcPath).getFileName().toString(); 224 final String[][] classes = new String[][] { 225 {"use.unsafe.UseUnsafeClass", "use.unsafe.UseUnsafeClass2"}, 226 {"use.indirect2.UseUnsafeIndirectly2", "use.unsafe.UseClassWithUnsafe"}, 227 {"use.indirect.UseUnsafeIndirectly"} 228 }; 229 final String[][] dependencies = new String[][] { 230 {"sun.misc.Unsafe"}, 231 {"use.unsafe.UseUnsafeClass"}, 232 {"use.unsafe.UseClassWithUnsafe"} 233 }; 234 final String[][] archives = new String[][] { 235 {arcName, arcPath, "JDK internal API (java.base)", "java.base"}, 236 {arcName, arcPath, arcName, arcPath}, 237 {arcName, arcPath, arcName, arcPath} 238 }; 239 return TestCaseData.make("sun.misc.Unsafe", arcPath, classes, dependencies, 240 archives, true); 241 } 242 243 static final List<Supplier<TestCaseData>> dataSuppliers = Arrays.asList( 244 JdepsDependencyClosure::makeTestCaseOne, 245 JdepsDependencyClosure::makeTestCaseTwo, 246 JdepsDependencyClosure::makeTestCaseThree, 247 JdepsDependencyClosure::makeTestCaseFour 248 ); 249 250 251 252 /** 253 * The OutputStreamParser is used to parse the format of jdeps. 254 * It is thus dependent on that format. 255 */ 256 static class OutputStreamParser extends OutputStream { 257 // OutputStreamParser will populate this map: 258 // 259 // For each archive, a list of class in where dependencies where 260 // found... 261 final Map<String, Set<String>> deps; 262 final StringBuilder text = new StringBuilder(); 263 264 StringBuilder[] lines = { new StringBuilder(), new StringBuilder() }; 265 int line = 0; 266 int sepi = 0; 267 char[] sep; 268 269 public OutputStreamParser(Map<String, Set<String>> deps) { 270 this.deps = deps; 271 this.sep = System.getProperty("line.separator").toCharArray(); 272 } 273 274 @Override 275 public void write(int b) throws IOException { 276 lines[line].append((char)b); 277 if (b == sep[sepi]) { 278 if (++sepi == sep.length) { 279 text.append(lines[line]); 280 if (lines[0].toString().startsWith(" ")) { 281 throw new RuntimeException("Bad formatting: " 282 + "summary line missing for\n"+lines[0]); 283 } 284 // Usually the output looks like that: 285 // <archive-1> -> java.base 286 // <class-1> -> <dependency> <dependency description> 287 // <class-2> -> <dependency> <dependency description> 288 // ... 289 // <archive-2> -> java.base 290 // <class-3> -> <dependency> <dependency description> 291 // <class-4> -> <dependency> <dependency description> 292 // ... 293 // 294 // We want to keep the <archive> line in lines[0] 295 // and have the ith <class-i> line in lines[1] 296 if (line == 1) { 297 // we have either a <class> line or an <archive> line. 298 String line1 = lines[0].toString(); 299 String line2 = lines[1].toString(); 300 if (line2.startsWith(" ")) { 301 // we have a class line, record it. 302 parse(line1, line2); 303 // prepare for next <class> line. 304 lines[1] = new StringBuilder(); 305 } else { 306 // We have an archive line: We are switching to the next archive. 307 // put the new <archive> line in lines[0], and prepare 308 // for reading the next <class> line 309 lines[0] = lines[1]; 310 lines[1] = new StringBuilder(); 311 } 312 } else { 313 // we just read the first <archive> line. 314 // prepare to read <class> lines. 315 line = 1; 316 } 317 sepi = 0; 318 } 319 } else { 320 sepi = 0; 321 } 322 } 323 324 // Takes a couple of lines, where line1 is an <archive> line and 325 // line 2 is a <class> line. Parses the line to extract the archive 326 // name and dependent class name, and record them in the map... 327 void parse(String line1, String line2) { 328 String archive = line1.substring(0, line1.indexOf(" -> ")); 329 int l2ArrowIndex = line2.indexOf(" -> "); 330 String className = line2.substring(2, l2ArrowIndex).replace(" ", ""); 331 String depdescr = line2.substring(l2ArrowIndex + 4); 332 String depclass = depdescr.substring(0, depdescr.indexOf(" ")); 333 deps.computeIfAbsent(archive, (k) -> new HashSet<>()); 334 deps.get(archive).add(className); 335 if (VERBOSE) { 336 System.out.println(archive+": "+className+" depends on "+depclass); 337 } 338 } 339 340 } 341 342 /** 343 * The main method. 344 * 345 * Can be run in two modes: 346 * <ul> 347 * <li>From jtreg: expects 1 argument in the form {@code --test:<test-nb>}</li> 348 * <li>From command line: expected syntax is {@code -e <pattern> -v jar [jars..]}</li> 349 * </ul> 350 * <p>When called from the command line this method will call jdeps recursively 351 * to build a closure of the dependencies on {@code <pattern>} and print a summary. 352 * <p>When called from jtreg - it will call jdeps either once only or 353 * recursively depending on the pattern. 354 * @param args either {@code --test:<test-nb>} or {@code -e <pattern> -v jar [jars..]}. 355 */ 356 public static void main(String[] args) { 357 runWithLocale(Locale.ENGLISH, TestCaseData.valueOf(args)::test); 358 } 359 360 private static void runWithLocale(Locale loc, Runnable run) { 361 final Locale defaultLocale = Locale.getDefault(); 362 Locale.setDefault(loc); 363 try { 364 run.run(); 365 } finally { 366 Locale.setDefault(defaultLocale); 367 } 368 } 369 370 371 public static void test(String[] args, Map<String, Set<String>> expected, 372 String expectedText, boolean closure) { 373 try { 374 doTest(args, expected, expectedText, closure); 375 } catch (Throwable t) { 376 try { 377 printDiagnostic(args, expectedText, t, closure); 378 } catch(Throwable tt) { 379 throw t; 380 } 381 throw t; 382 } 383 } 384 385 static class TextFormatException extends RuntimeException { 386 final String expected; 387 final String actual; 388 TextFormatException(String message, String expected, String actual) { 389 super(message); 390 this.expected = expected; 391 this.actual = actual; 392 } 393 } 394 395 public static void printDiagnostic(String[] args, String expectedText, 396 Throwable t, boolean closure) { 397 if (expectedText != null || t instanceof TextFormatException) { 398 System.err.println("===== TEST FAILED ======="); 399 System.err.println("command: " + Stream.of(args) 400 .reduce("jdeps", (s1,s2) -> s1.concat(" ").concat(s2))); 401 System.err.println("===== Expected Output ======="); 402 System.err.append(expectedText); 403 System.err.println("===== Command Output ======="); 404 if (t instanceof TextFormatException) { 405 System.err.print(((TextFormatException)t).actual); 406 } else { 407 com.sun.tools.jdeps.Main.run(args, new PrintWriter(System.err)); 408 if (closure) System.err.println("... (closure not available) ..."); 409 } 410 System.err.println("============================="); 411 } 412 } 413 414 public static void doTest(String[] args, Map<String, Set<String>> expected, 415 String expectedText, boolean closure) { 416 if (args.length < 3 || !"-e".equals(args[0]) || !"-v".equals(args[2])) { 417 System.err.println("Syntax: -e <classname> -v [list of jars or directories]"); 418 return; 419 } 420 Map<String, Map<String, Set<String>>> alldeps = new HashMap<>(); 421 String depName = args[1]; 422 List<String> search = new ArrayList<>(); 423 search.add(depName); 424 Set<String> searched = new LinkedHashSet<>(); 425 StringBuilder text = new StringBuilder(); 426 while(!search.isEmpty()) { 427 args[1] = search.remove(0); 428 if (VERBOSE) { 429 System.out.println("Looking for " + args[1]); 430 } 431 searched.add(args[1]); 432 Map<String, Set<String>> deps = 433 alldeps.computeIfAbsent(args[1], (k) -> new HashMap<>()); 434 OutputStreamParser parser = new OutputStreamParser(deps); 435 PrintWriter writer = new PrintWriter(parser); 436 com.sun.tools.jdeps.Main.run(args, writer); 437 if (VERBOSE) { 438 System.out.println("Found: " + deps.values().stream() 439 .flatMap(s -> s.stream()).collect(Collectors.toSet())); 440 } 441 if (expectedText != null) { 442 text.append(parser.text.toString()); 443 } 444 search.addAll(deps.values().stream() 445 .flatMap(s -> s.stream()) 446 .filter(k -> !searched.contains(k)) 447 .collect(Collectors.toSet())); 448 if (!closure) break; 449 } 450 451 // Print summary... 452 final Set<String> classes = alldeps.values().stream() 453 .flatMap((m) -> m.values().stream()) 454 .flatMap(s -> s.stream()).collect(Collectors.toSet()); 455 Map<String, Set<String>> result = new HashMap<>(); 456 for (String c : classes) { 457 Set<String> archives = new HashSet<>(); 458 Set<String> dependencies = new HashSet<>(); 459 for (String d : alldeps.keySet()) { 460 Map<String, Set<String>> m = alldeps.get(d); 461 for (String a : m.keySet()) { 462 Set<String> s = m.get(a); 463 if (s.contains(c)) { 464 archives.add(a); 465 dependencies.add(d); 466 } 467 } 468 } 469 result.put(c, dependencies); 470 System.out.println(c + " " + archives + " depends on " + dependencies); 471 } 472 473 // If we're in jtreg, then check result (expectedText != null) 474 if (expectedText != null && COMPARE_TEXT) { 475 //text.append(String.format("%n")); 476 if (text.toString().equals(expectedText)) { 477 System.out.println("SUCCESS - got expected text"); 478 } else { 479 throw new TextFormatException("jdeps output is not as expected", 480 expectedText, text.toString()); 481 } 482 } 483 if (expected != null) { 484 if (expected.equals(result)) { 485 System.out.println("SUCCESS - found expected dependencies"); 486 } else if (expectedText == null) { 487 throw new RuntimeException("Bad dependencies: Expected " + expected 488 + " but found " + result); 489 } else { 490 throw new TextFormatException("Bad dependencies: Expected " 491 + expected 492 + " but found " + result, 493 expectedText, text.toString()); 494 } 495 } 496 } 497} 498