blob: dfba14357e9e7c1d4b450f1b7af77b7c2c32eba5 [file] [log] [blame]
/*
* 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());
}
}
}
}