3789983e89
Reviewed-by: darcy, ihse
499 lines
22 KiB
Java
499 lines
22 KiB
Java
/*
|
|
* 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());
|
|
}
|
|
}
|
|
}
|
|
}
|