Path: blob/master/test/langtools/tools/jdeps/VerboseFormat/JdepsDependencyClosure.java
41149 views
/*1* Copyright (c) 2015, Oracle and/or its affiliates. All rights reserved.2* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.3*4* This code is free software; you can redistribute it and/or modify it5* under the terms of the GNU General Public License version 2 only, as6* published by the Free Software Foundation.7*8* This code is distributed in the hope that it will be useful, but WITHOUT9* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or10* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License11* version 2 for more details (a copy is included in the LICENSE file that12* accompanied this code).13*14* You should have received a copy of the GNU General Public License version15* 2 along with this work; if not, write to the Free Software Foundation,16* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.17*18* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA19* or visit www.oracle.com if you need additional information or have any20* questions.21*/2223import java.io.IOException;24import java.io.OutputStream;25import java.io.PrintWriter;26import java.nio.file.Paths;27import java.util.ArrayList;28import java.util.Arrays;29import java.util.HashMap;30import java.util.HashSet;31import java.util.LinkedHashSet;32import java.util.List;33import java.util.Locale;34import java.util.Map;35import java.util.Set;36import java.util.function.Supplier;37import java.util.stream.Collectors;38import java.util.stream.Stream;3940/**41* @test42* @bug 808060843* @summary Test that jdeps verbose output has a summary line when dependencies44* are found within the same archive. For each testcase, compare the45* result obtained from jdeps with the expected result.46* @modules jdk.jdeps/com.sun.tools.jdeps47* java.base/sun.security.x50948* @build use.indirect.DontUseJdkInternal249* @build use.indirect.UseJdkInternalIndirectly50* @build use.indirect2.DontUseJdkInternal351* @build use.indirect2.UseJdkInternalIndirectly252* @build use.internal.DontUseJdkInternal53* @build use.internal.UseClassWithJdkInternal54* @build use.internal.UseJdkInternalClass55* @build use.internal.UseJdkInternalClass256* @run main JdepsDependencyClosure --test:057* @run main JdepsDependencyClosure --test:158* @run main JdepsDependencyClosure --test:259* @run main JdepsDependencyClosure --test:360*/61public class JdepsDependencyClosure {6263static boolean VERBOSE = false;64static boolean COMPARE_TEXT = true;6566static final String JDEPS_SUMMARY_TEXT_FORMAT = "%s -> %s%n";67static final String JDEPS_VERBOSE_TEXT_FORMAT = " %-50s -> %-50s %s%n";6869/**70* Helper class used to store arguments to pass to71* {@code JdepsDependencyClosure.test} as well as expected72* results.73*/74static class TestCaseData {75final Map<String, Set<String>> expectedDependencies;76final String expectedText;77final String[] args;78final boolean closure;7980TestCaseData(Map<String, Set<String>> expectedDependencies,81String expectedText,82boolean closure,83String[] args) {84this.expectedDependencies = expectedDependencies;85this.expectedText = expectedText;86this.closure = closure;87this.args = args;88}8990public void test() {91if (expectedDependencies != null) {92String format = closure93? "Running (closure): jdeps %s %s %s %s"94: "Running: jdeps %s %s %s %s";95System.out.println(String.format(format, (Object[])args));96}97JdepsDependencyClosure.test(args, expectedDependencies, expectedText, closure);98}99100/**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 -e103* 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, the106* expected list of classes in the source that will107* be reported as having a dependency on the class108* in the target that matches the given pattern.109* @param dependencies For each archive dependency couple, a singleton list110* containing the name of the class in the target that111* matches the pattern. It is expected that the pattern112* will match only one class in the target.113* If the pattern matches several classes the114* expected text may no longer match the jdeps output.115* @param archives A list of archive dependency couple in the form116* {{sourceName1, sourcePath1, targetDescription1, targetPath1}117* {sourceName2, sourcePath2, targetDescription2, targetPath2}118* ... }119* For a JDK module - e.g. java.base, the targetDescription120* 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 build123* the closure.124* @return An instance of TestCaseData containing all the information125* needed to perform the jdeps invokation and test its output.126*/127public static TestCaseData make(String pattern, String arcPath, String[][] classes,128String[][] dependencies, String[][] archives, boolean closure) {129final String[] args = new String[] {130"-e", pattern, "-v", arcPath131};132Map<String, Set<String>> expected = new HashMap<>();133String expectedText = "";134for (int i=0; i<classes.length; i++) {135final int index = i;136expectedText += Stream.of(classes[i])137.map((cn) -> String.format(JDEPS_VERBOSE_TEXT_FORMAT, cn,138dependencies[index][0], archives[index][2]))139.reduce(String.format(JDEPS_SUMMARY_TEXT_FORMAT, archives[i][0],140archives[index][3]), (s1,s2) -> s1.concat(s2));141for (String cn : classes[index]) {142expected.putIfAbsent(cn, new HashSet<>());143expected.get(cn).add(dependencies[index][0]);144}145}146return new TestCaseData(expected, expectedText, closure, args);147}148149public static TestCaseData valueOf(String[] args) {150if (args.length == 1 && args[0].startsWith("--test:")) {151// invoked from jtreg. build test case data for selected test.152int index = Integer.parseInt(args[0].substring("--test:".length()));153if (index >= dataSuppliers.size()) {154throw new RuntimeException("No such test case: " + index155+ " - available testcases are [0.."156+ (dataSuppliers.size()-1) + "]");157}158return dataSuppliers.get(index).get();159} else {160// invoked in standalone. just take the given argument161// and perform no validation on the output (except that it162// must start with a summary line)163return new TestCaseData(null, null, true, args);164}165}166167}168169static TestCaseData makeTestCaseOne() {170final String arcPath = System.getProperty("test.classes", "build/classes");171final String arcName = Paths.get(arcPath).getFileName().toString();172final String[][] classes = new String[][] {173{"use.indirect2.UseJdkInternalIndirectly2", "use.internal.UseClassWithJdkInternal"},174};175final String[][] dependencies = new String[][] {176{"use.internal.UseJdkInternalClass"},177};178final String[][] archives = new String[][] {179{arcName, arcPath, arcName, arcPath},180};181return TestCaseData.make("use.internal.UseJdkInternalClass", arcPath, classes,182dependencies, archives, false);183}184185static TestCaseData makeTestCaseTwo() {186String arcPath = System.getProperty("test.classes", "build/classes");187String arcName = Paths.get(arcPath).getFileName().toString();188String[][] classes = new String[][] {189{"use.internal.UseJdkInternalClass", "use.internal.UseJdkInternalClass2"}190};191String[][] dependencies = new String[][] {192{"sun.security.x509.X509CertInfo"}193};194String[][] archive = new String[][] {195{arcName, arcPath, "JDK internal API (java.base)", "java.base"},196};197return TestCaseData.make("sun.security.x509.X509CertInfo", arcPath, classes,198dependencies, archive, false);199}200201static TestCaseData makeTestCaseThree() {202final String arcPath = System.getProperty("test.classes", "build/classes");203final String arcName = Paths.get(arcPath).getFileName().toString();204final String[][] classes = new String[][] {205{"use.indirect2.UseJdkInternalIndirectly2", "use.internal.UseClassWithJdkInternal"},206{"use.indirect.UseJdkInternalIndirectly"}207};208final String[][] dependencies = new String[][] {209{"use.internal.UseJdkInternalClass"},210{"use.internal.UseClassWithJdkInternal"}211};212final String[][] archives = new String[][] {213{arcName, arcPath, arcName, arcPath},214{arcName, arcPath, arcName, arcPath}215};216return TestCaseData.make("use.internal.UseJdkInternalClass", arcPath, classes,217dependencies, archives, true);218}219220221static TestCaseData makeTestCaseFour() {222final String arcPath = System.getProperty("test.classes", "build/classes");223final String arcName = Paths.get(arcPath).getFileName().toString();224final String[][] classes = new String[][] {225{"use.internal.UseJdkInternalClass", "use.internal.UseJdkInternalClass2"},226{"use.indirect2.UseJdkInternalIndirectly2", "use.internal.UseClassWithJdkInternal"},227{"use.indirect.UseJdkInternalIndirectly"}228};229final String[][] dependencies = new String[][] {230{"sun.security.x509.X509CertInfo"},231{"use.internal.UseJdkInternalClass"},232{"use.internal.UseClassWithJdkInternal"}233};234final 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};239return TestCaseData.make("sun.security.x509.X509CertInfo", arcPath, classes, dependencies,240archives, true);241}242243static final List<Supplier<TestCaseData>> dataSuppliers = Arrays.asList(244JdepsDependencyClosure::makeTestCaseOne,245JdepsDependencyClosure::makeTestCaseTwo,246JdepsDependencyClosure::makeTestCaseThree,247JdepsDependencyClosure::makeTestCaseFour248);249250251252/**253* The OutputStreamParser is used to parse the format of jdeps.254* It is thus dependent on that format.255*/256static class OutputStreamParser extends OutputStream {257// OutputStreamParser will populate this map:258//259// For each archive, a list of class in where dependencies where260// found...261final Map<String, Set<String>> deps;262final StringBuilder text = new StringBuilder();263264StringBuilder[] lines = { new StringBuilder(), new StringBuilder() };265int line = 0;266int sepi = 0;267char[] sep;268269public OutputStreamParser(Map<String, Set<String>> deps) {270this.deps = deps;271this.sep = System.getProperty("line.separator").toCharArray();272}273274@Override275public void write(int b) throws IOException {276lines[line].append((char)b);277if (b == sep[sepi]) {278if (++sepi == sep.length) {279text.append(lines[line]);280if (lines[0].toString().startsWith(" ")) {281throw new RuntimeException("Bad formatting: "282+ "summary line missing for\n"+lines[0]);283}284// Usually the output looks like that:285// <archive-1> -> java.base286// <class-1> -> <dependency> <dependency description>287// <class-2> -> <dependency> <dependency description>288// ...289// <archive-2> -> java.base290// <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]296if (line == 1) {297// we have either a <class> line or an <archive> line.298String line1 = lines[0].toString();299String line2 = lines[1].toString();300if (line2.startsWith(" ")) {301// we have a class line, record it.302parse(line1, line2);303// prepare for next <class> line.304lines[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 prepare308// for reading the next <class> line309lines[0] = lines[1];310lines[1] = new StringBuilder();311}312} else {313// we just read the first <archive> line.314// prepare to read <class> lines.315line = 1;316}317sepi = 0;318}319} else {320sepi = 0;321}322}323324// Takes a couple of lines, where line1 is an <archive> line and325// line 2 is a <class> line. Parses the line to extract the archive326// name and dependent class name, and record them in the map...327void parse(String line1, String line2) {328String archive = line1.substring(0, line1.indexOf(" -> "));329int l2ArrowIndex = line2.indexOf(" -> ");330String className = line2.substring(2, l2ArrowIndex).replace(" ", "");331String depdescr = line2.substring(l2ArrowIndex + 4);332String depclass = depdescr.substring(0, depdescr.indexOf(" "));333deps.computeIfAbsent(archive, (k) -> new HashSet<>());334deps.get(archive).add(className);335if (VERBOSE) {336System.out.println(archive+": "+className+" depends on "+depclass);337}338}339340}341342/**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 recursively351* 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 or353* recursively depending on the pattern.354* @param args either {@code --test:<test-nb>} or {@code -e <pattern> -v jar [jars..]}.355*/356public static void main(String[] args) {357runWithLocale(Locale.ENGLISH, TestCaseData.valueOf(args)::test);358}359360private static void runWithLocale(Locale loc, Runnable run) {361final Locale defaultLocale = Locale.getDefault();362Locale.setDefault(loc);363try {364run.run();365} finally {366Locale.setDefault(defaultLocale);367}368}369370371public static void test(String[] args, Map<String, Set<String>> expected,372String expectedText, boolean closure) {373try {374doTest(args, expected, expectedText, closure);375} catch (Throwable t) {376try {377printDiagnostic(args, expectedText, t, closure);378} catch(Throwable tt) {379throw t;380}381throw t;382}383}384385static class TextFormatException extends RuntimeException {386final String expected;387final String actual;388TextFormatException(String message, String expected, String actual) {389super(message);390this.expected = expected;391this.actual = actual;392}393}394395public static void printDiagnostic(String[] args, String expectedText,396Throwable t, boolean closure) {397if (expectedText != null || t instanceof TextFormatException) {398System.err.println("===== TEST FAILED =======");399System.err.println("command: " + Stream.of(args)400.reduce("jdeps", (s1,s2) -> s1.concat(" ").concat(s2)));401System.err.println("===== Expected Output =======");402System.err.append(expectedText);403System.err.println("===== Command Output =======");404if (t instanceof TextFormatException) {405System.err.print(((TextFormatException)t).actual);406} else {407com.sun.tools.jdeps.Main.run(args, new PrintWriter(System.err));408if (closure) System.err.println("... (closure not available) ...");409}410System.err.println("=============================");411}412}413414public static void doTest(String[] args, Map<String, Set<String>> expected,415String expectedText, boolean closure) {416if (args.length < 3 || !"-e".equals(args[0]) || !"-v".equals(args[2])) {417System.err.println("Syntax: -e <classname> -v [list of jars or directories]");418return;419}420Map<String, Map<String, Set<String>>> alldeps = new HashMap<>();421String depName = args[1];422List<String> search = new ArrayList<>();423search.add(depName);424Set<String> searched = new LinkedHashSet<>();425StringBuilder text = new StringBuilder();426while(!search.isEmpty()) {427args[1] = search.remove(0);428if (VERBOSE) {429System.out.println("Looking for " + args[1]);430}431searched.add(args[1]);432Map<String, Set<String>> deps =433alldeps.computeIfAbsent(args[1], (k) -> new HashMap<>());434OutputStreamParser parser = new OutputStreamParser(deps);435PrintWriter writer = new PrintWriter(parser);436com.sun.tools.jdeps.Main.run(args, writer);437if (VERBOSE) {438System.out.println("Found: " + deps.values().stream()439.flatMap(s -> s.stream()).collect(Collectors.toSet()));440}441if (expectedText != null) {442text.append(parser.text.toString());443}444search.addAll(deps.values().stream()445.flatMap(s -> s.stream())446.filter(k -> !searched.contains(k))447.collect(Collectors.toSet()));448if (!closure) break;449}450451// Print summary...452final Set<String> classes = alldeps.values().stream()453.flatMap((m) -> m.values().stream())454.flatMap(s -> s.stream()).collect(Collectors.toSet());455Map<String, Set<String>> result = new HashMap<>();456for (String c : classes) {457Set<String> archives = new HashSet<>();458Set<String> dependencies = new HashSet<>();459for (String d : alldeps.keySet()) {460Map<String, Set<String>> m = alldeps.get(d);461for (String a : m.keySet()) {462Set<String> s = m.get(a);463if (s.contains(c)) {464archives.add(a);465dependencies.add(d);466}467}468}469result.put(c, dependencies);470System.out.println(c + " " + archives + " depends on " + dependencies);471}472473// If we're in jtreg, then check result (expectedText != null)474if (expectedText != null && COMPARE_TEXT) {475//text.append(String.format("%n"));476if (text.toString().equals(expectedText)) {477System.out.println("SUCCESS - got expected text");478} else {479throw new TextFormatException("jdeps output is not as expected",480expectedText, text.toString());481}482}483if (expected != null) {484if (expected.equals(result)) {485System.out.println("SUCCESS - found expected dependencies");486} else if (expectedText == null) {487throw new RuntimeException("Bad dependencies: Expected " + expected488+ " but found " + result);489} else {490throw new TextFormatException("Bad dependencies: Expected "491+ expected492+ " but found " + result,493expectedText, text.toString());494}495}496}497}498499500