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 }