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