/* * Copyright (c) 2015, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ import java.io.IOException; import java.io.OutputStream; import java.io.PrintWriter; import java.nio.file.Paths; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.function.Supplier; import java.util.stream.Collectors; import java.util.stream.Stream; /** * @test * @bug 8080608 * @summary Test that jdeps verbose output has a summary line when dependencies * are found within the same archive. For each testcase, compare the * result obtained from jdeps with the expected result. * @modules jdk.jdeps/com.sun.tools.jdeps * java.base/sun.security.x509 * @build use.indirect.DontUseJdkInternal2 * @build use.indirect.UseJdkInternalIndirectly * @build use.indirect2.DontUseJdkInternal3 * @build use.indirect2.UseJdkInternalIndirectly2 * @build use.internal.DontUseJdkInternal * @build use.internal.UseClassWithJdkInternal * @build use.internal.UseJdkInternalClass * @build use.internal.UseJdkInternalClass2 * @run main JdepsDependencyClosure --test:0 * @run main JdepsDependencyClosure --test:1 * @run main JdepsDependencyClosure --test:2 * @run main JdepsDependencyClosure --test:3 */ public class JdepsDependencyClosure { static boolean VERBOSE = false; static boolean COMPARE_TEXT = true; static final String JDEPS_SUMMARY_TEXT_FORMAT = "%s -> %s%n"; static final String JDEPS_VERBOSE_TEXT_FORMAT = " %-50s -> %-50s %s%n"; /** * Helper class used to store arguments to pass to * {@code JdepsDependencyClosure.test} as well as expected * results. */ static class TestCaseData { final Map<String, Set<String>> expectedDependencies; final String expectedText; final String[] args; final boolean closure; TestCaseData(Map<String, Set<String>> expectedDependencies, String expectedText, boolean closure, String[] args) { this.expectedDependencies = expectedDependencies; this.expectedText = expectedText; this.closure = closure; this.args = args; } public void test() { if (expectedDependencies != null) { String format = closure ? "Running (closure): jdeps %s %s %s %s" : "Running: jdeps %s %s %s %s"; System.out.println(String.format(format, (Object[])args)); } JdepsDependencyClosure.test(args, expectedDependencies, expectedText, closure); } /** * Make a new test case data to invoke jdeps and test its output. * @param pattern The pattern that will passed through to jdeps -e * This is expected to match only one class. * @param arcPath The archive to analyze. A jar or a class directory. * @param classes For each reported archive dependency couple, the * expected list of classes in the source that will * be reported as having a dependency on the class * in the target that matches the given pattern. * @param dependencies For each archive dependency couple, a singleton list * containing the name of the class in the target that * matches the pattern. It is expected that the pattern * will match only one class in the target. * If the pattern matches several classes the * expected text may no longer match the jdeps output. * @param archives A list of archive dependency couple in the form * {{sourceName1, sourcePath1, targetDescription1, targetPath1} * {sourceName2, sourcePath2, targetDescription2, targetPath2} * ... } * For a JDK module - e.g. java.base, the targetDescription * is usually something like "JDK internal API (java.base)" * and the targetPath is usually the module name "java.base". * @param closure Whether jdeps should be recursively invoked to build * the closure. * @return An instance of TestCaseData containing all the information * needed to perform the jdeps invokation and test its output. */ public static TestCaseData make(String pattern, String arcPath, String[][] classes, String[][] dependencies, String[][] archives, boolean closure) { final String[] args = new String[] { "-e", pattern, "-v", arcPath }; Map<String, Set<String>> expected = new HashMap<>(); String expectedText = ""; for (int i=0; i<classes.length; i++) { final int index = i; expectedText += Stream.of(classes[i]) .map((cn) -> String.format(JDEPS_VERBOSE_TEXT_FORMAT, cn, dependencies[index][0], archives[index][2])) .reduce(String.format(JDEPS_SUMMARY_TEXT_FORMAT, archives[i][0], archives[index][3]), (s1,s2) -> s1.concat(s2)); for (String cn : classes[index]) { expected.putIfAbsent(cn, new HashSet<>()); expected.get(cn).add(dependencies[index][0]); } } return new TestCaseData(expected, expectedText, closure, args); } public static TestCaseData valueOf(String[] args) { if (args.length == 1 && args[0].startsWith("--test:")) { // invoked from jtreg. build test case data for selected test. int index = Integer.parseInt(args[0].substring("--test:".length())); if (index >= dataSuppliers.size()) { throw new RuntimeException("No such test case: " + index + " - available testcases are [0.." + (dataSuppliers.size()-1) + "]"); } return dataSuppliers.get(index).get(); } else { // invoked in standalone. just take the given argument // and perform no validation on the output (except that it // must start with a summary line) return new TestCaseData(null, null, true, args); } } } static TestCaseData makeTestCaseOne() { final String arcPath = System.getProperty("test.classes", "build/classes"); final String arcName = Paths.get(arcPath).getFileName().toString(); final String[][] classes = new String[][] { {"use.indirect2.UseJdkInternalIndirectly2", "use.internal.UseClassWithJdkInternal"}, }; final String[][] dependencies = new String[][] { {"use.internal.UseJdkInternalClass"}, }; final String[][] archives = new String[][] { {arcName, arcPath, arcName, arcPath}, }; return TestCaseData.make("use.internal.UseJdkInternalClass", arcPath, classes, dependencies, archives, false); } static TestCaseData makeTestCaseTwo() { String arcPath = System.getProperty("test.classes", "build/classes"); String arcName = Paths.get(arcPath).getFileName().toString(); String[][] classes = new String[][] { {"use.internal.UseJdkInternalClass", "use.internal.UseJdkInternalClass2"} }; String[][] dependencies = new String[][] { {"sun.security.x509.X509CertInfo"} }; String[][] archive = new String[][] { {arcName, arcPath, "JDK internal API (java.base)", "java.base"}, }; return TestCaseData.make("sun.security.x509.X509CertInfo", arcPath, classes, dependencies, archive, false); } static TestCaseData makeTestCaseThree() { final String arcPath = System.getProperty("test.classes", "build/classes"); final String arcName = Paths.get(arcPath).getFileName().toString(); final String[][] classes = new String[][] { {"use.indirect2.UseJdkInternalIndirectly2", "use.internal.UseClassWithJdkInternal"}, {"use.indirect.UseJdkInternalIndirectly"} }; final String[][] dependencies = new String[][] { {"use.internal.UseJdkInternalClass"}, {"use.internal.UseClassWithJdkInternal"} }; final String[][] archives = new String[][] { {arcName, arcPath, arcName, arcPath}, {arcName, arcPath, arcName, arcPath} }; return TestCaseData.make("use.internal.UseJdkInternalClass", arcPath, classes, dependencies, archives, true); } static TestCaseData makeTestCaseFour() { final String arcPath = System.getProperty("test.classes", "build/classes"); final String arcName = Paths.get(arcPath).getFileName().toString(); final String[][] classes = new String[][] { {"use.internal.UseJdkInternalClass", "use.internal.UseJdkInternalClass2"}, {"use.indirect2.UseJdkInternalIndirectly2", "use.internal.UseClassWithJdkInternal"}, {"use.indirect.UseJdkInternalIndirectly"} }; final String[][] dependencies = new String[][] { {"sun.security.x509.X509CertInfo"}, {"use.internal.UseJdkInternalClass"}, {"use.internal.UseClassWithJdkInternal"} }; final String[][] archives = new String[][] { {arcName, arcPath, "JDK internal API (java.base)", "java.base"}, {arcName, arcPath, arcName, arcPath}, {arcName, arcPath, arcName, arcPath} }; return TestCaseData.make("sun.security.x509.X509CertInfo", arcPath, classes, dependencies, archives, true); } static final List<Supplier<TestCaseData>> dataSuppliers = Arrays.asList( JdepsDependencyClosure::makeTestCaseOne, JdepsDependencyClosure::makeTestCaseTwo, JdepsDependencyClosure::makeTestCaseThree, JdepsDependencyClosure::makeTestCaseFour ); /** * The OutputStreamParser is used to parse the format of jdeps. * It is thus dependent on that format. */ static class OutputStreamParser extends OutputStream { // OutputStreamParser will populate this map: // // For each archive, a list of class in where dependencies where // found... final Map<String, Set<String>> deps; final StringBuilder text = new StringBuilder(); StringBuilder[] lines = { new StringBuilder(), new StringBuilder() }; int line = 0; int sepi = 0; char[] sep; public OutputStreamParser(Map<String, Set<String>> deps) { this.deps = deps; this.sep = System.getProperty("line.separator").toCharArray(); } @Override public void write(int b) throws IOException { lines[line].append((char)b); if (b == sep[sepi]) { if (++sepi == sep.length) { text.append(lines[line]); if (lines[0].toString().startsWith(" ")) { throw new RuntimeException("Bad formatting: " + "summary line missing for\n"+lines[0]); } // Usually the output looks like that: // <archive-1> -> java.base // <class-1> -> <dependency> <dependency description> // <class-2> -> <dependency> <dependency description> // ... // <archive-2> -> java.base // <class-3> -> <dependency> <dependency description> // <class-4> -> <dependency> <dependency description> // ... // // We want to keep the <archive> line in lines[0] // and have the ith <class-i> line in lines[1] if (line == 1) { // we have either a <class> line or an <archive> line. String line1 = lines[0].toString(); String line2 = lines[1].toString(); if (line2.startsWith(" ")) { // we have a class line, record it. parse(line1, line2); // prepare for next <class> line. lines[1] = new StringBuilder(); } else { // We have an archive line: We are switching to the next archive. // put the new <archive> line in lines[0], and prepare // for reading the next <class> line lines[0] = lines[1]; lines[1] = new StringBuilder(); } } else { // we just read the first <archive> line. // prepare to read <class> lines. line = 1; } sepi = 0; } } else { sepi = 0; } } // Takes a couple of lines, where line1 is an <archive> line and // line 2 is a <class> line. Parses the line to extract the archive // name and dependent class name, and record them in the map... void parse(String line1, String line2) { String archive = line1.substring(0, line1.indexOf(" -> ")); int l2ArrowIndex = line2.indexOf(" -> "); String className = line2.substring(2, l2ArrowIndex).replace(" ", ""); String depdescr = line2.substring(l2ArrowIndex + 4); String depclass = depdescr.substring(0, depdescr.indexOf(" ")); deps.computeIfAbsent(archive, (k) -> new HashSet<>()); deps.get(archive).add(className); if (VERBOSE) { System.out.println(archive+": "+className+" depends on "+depclass); } } } /** * The main method. * * Can be run in two modes: * <ul> * <li>From jtreg: expects 1 argument in the form {@code --test:<test-nb>}</li> * <li>From command line: expected syntax is {@code -e <pattern> -v jar [jars..]}</li> * </ul> * <p>When called from the command line this method will call jdeps recursively * to build a closure of the dependencies on {@code <pattern>} and print a summary. * <p>When called from jtreg - it will call jdeps either once only or * recursively depending on the pattern. * @param args either {@code --test:<test-nb>} or {@code -e <pattern> -v jar [jars..]}. */ public static void main(String[] args) { runWithLocale(Locale.ENGLISH, TestCaseData.valueOf(args)::test); } private static void runWithLocale(Locale loc, Runnable run) { final Locale defaultLocale = Locale.getDefault(); Locale.setDefault(loc); try { run.run(); } finally { Locale.setDefault(defaultLocale); } } public static void test(String[] args, Map<String, Set<String>> expected, String expectedText, boolean closure) { try { doTest(args, expected, expectedText, closure); } catch (Throwable t) { try { printDiagnostic(args, expectedText, t, closure); } catch(Throwable tt) { throw t; } throw t; } } static class TextFormatException extends RuntimeException { final String expected; final String actual; TextFormatException(String message, String expected, String actual) { super(message); this.expected = expected; this.actual = actual; } } public static void printDiagnostic(String[] args, String expectedText, Throwable t, boolean closure) { if (expectedText != null || t instanceof TextFormatException) { System.err.println("===== TEST FAILED ======="); System.err.println("command: " + Stream.of(args) .reduce("jdeps", (s1,s2) -> s1.concat(" ").concat(s2))); System.err.println("===== Expected Output ======="); System.err.append(expectedText); System.err.println("===== Command Output ======="); if (t instanceof TextFormatException) { System.err.print(((TextFormatException)t).actual); } else { com.sun.tools.jdeps.Main.run(args, new PrintWriter(System.err)); if (closure) System.err.println("... (closure not available) ..."); } System.err.println("============================="); } } public static void doTest(String[] args, Map<String, Set<String>> expected, String expectedText, boolean closure) { if (args.length < 3 || !"-e".equals(args[0]) || !"-v".equals(args[2])) { System.err.println("Syntax: -e <classname> -v [list of jars or directories]"); return; } Map<String, Map<String, Set<String>>> alldeps = new HashMap<>(); String depName = args[1]; List<String> search = new ArrayList<>(); search.add(depName); Set<String> searched = new LinkedHashSet<>(); StringBuilder text = new StringBuilder(); while(!search.isEmpty()) { args[1] = search.remove(0); if (VERBOSE) { System.out.println("Looking for " + args[1]); } searched.add(args[1]); Map<String, Set<String>> deps = alldeps.computeIfAbsent(args[1], (k) -> new HashMap<>()); OutputStreamParser parser = new OutputStreamParser(deps); PrintWriter writer = new PrintWriter(parser); com.sun.tools.jdeps.Main.run(args, writer); if (VERBOSE) { System.out.println("Found: " + deps.values().stream() .flatMap(s -> s.stream()).collect(Collectors.toSet())); } if (expectedText != null) { text.append(parser.text.toString()); } search.addAll(deps.values().stream() .flatMap(s -> s.stream()) .filter(k -> !searched.contains(k)) .collect(Collectors.toSet())); if (!closure) break; } // Print summary... final Set<String> classes = alldeps.values().stream() .flatMap((m) -> m.values().stream()) .flatMap(s -> s.stream()).collect(Collectors.toSet()); Map<String, Set<String>> result = new HashMap<>(); for (String c : classes) { Set<String> archives = new HashSet<>(); Set<String> dependencies = new HashSet<>(); for (String d : alldeps.keySet()) { Map<String, Set<String>> m = alldeps.get(d); for (String a : m.keySet()) { Set<String> s = m.get(a); if (s.contains(c)) { archives.add(a); dependencies.add(d); } } } result.put(c, dependencies); System.out.println(c + " " + archives + " depends on " + dependencies); } // If we're in jtreg, then check result (expectedText != null) if (expectedText != null && COMPARE_TEXT) { //text.append(String.format("%n")); if (text.toString().equals(expectedText)) { System.out.println("SUCCESS - got expected text"); } else { throw new TextFormatException("jdeps output is not as expected", expectedText, text.toString()); } } if (expected != null) { if (expected.equals(result)) { System.out.println("SUCCESS - found expected dependencies"); } else if (expectedText == null) { throw new RuntimeException("Bad dependencies: Expected " + expected + " but found " + result); } else { throw new TextFormatException("Bad dependencies: Expected " + expected + " but found " + result, expectedText, text.toString()); } } } }