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