8273154: Provide a JavadocTester method for non-overlapping, unordered output matching

Reviewed-by: prappo
This commit is contained in:
Jonathan Gibbons 2021-11-10 15:24:27 +00:00
parent f561d3c194
commit ce3ed65ac3
2 changed files with 982 additions and 154 deletions
test/langtools/jdk/javadoc
lib/javadoc/tester
testJavadocTester

@ -23,7 +23,6 @@
package javadoc.tester;
import java.io.BufferedWriter;
import java.io.ByteArrayOutputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
@ -39,12 +38,10 @@ import java.lang.reflect.Method;
import java.nio.charset.Charset;
import java.nio.charset.UnsupportedCharsetException;
import java.nio.file.DirectoryStream;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
@ -53,11 +50,14 @@ import java.util.Comparator;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
/**
@ -183,6 +183,7 @@ public abstract class JavadocTester {
/**
* Get the path for a source file in the test source directory.
*
* @param path the path of a file or directory in the source directory
* @return the full path of the specified file
*/
@ -264,6 +265,7 @@ public abstract class JavadocTester {
/**
* Run all methods annotated with @Test, followed by printSummary.
* Typically called on a tester object in main()
*
* @throws Exception if any errors occurred
*/
public void runTests() throws Exception {
@ -273,6 +275,7 @@ public abstract class JavadocTester {
/**
* Runs all methods annotated with @Test, followed by printSummary.
* Typically called on a tester object in main()
*
* @param f a function which will be used to provide arguments to each
* invoked method
* @throws Exception if any errors occurred
@ -302,12 +305,12 @@ public abstract class JavadocTester {
* put each option and the arguments it takes on a separate line.
*
* Example:
* <pre><code>
* {@snippet :
* javadoc("-d", "out",
* "-sourcepath", testSrc,
* "-notimestamp",
* "pkg1", "pkg2", "pkg3/C.java");
* </code></pre>
* }
*
* @param args the arguments to pass to javadoc
*/
@ -401,6 +404,7 @@ public abstract class JavadocTester {
* Sets the kind of check for the initial contents of the output directory
* before javadoc is run.
* The filter should return true for files that should <b>not</b> appear.
*
* @param c the kind of check to perform
*/
public void setOutputDirectoryCheck(DirectoryCheck c) {
@ -464,8 +468,7 @@ public abstract class JavadocTester {
/**
* Checks the exit code of the most recent call of javadoc.
*
* @param expected the exit code that is required for the test
* to pass.
* @param expected the exit code that is required for the test to pass
*/
public void checkExit(Exit expected) {
checking("check exit code");
@ -480,12 +483,13 @@ public abstract class JavadocTester {
* Checks for content in (or not in) the generated output.
* Within the search strings, the newline character \n
* will be translated to the platform newline character sequence.
* @param path a path within the most recent output directory
* or the name of one of the output buffers, identifying
* where to look for the search strings.
*
* @param path a path within the most recent output directory
* or the name of one of the output buffers, identifying
* where to look for the search strings.
* @param expectedFound true if all of the search strings are expected
* to be found, or false if the file is not expected to be found
* @param strings the strings to be searched for
* to be found, or false if the file is not expected to be found
* @param strings the strings to be searched for
*/
public void checkFileAndOutput(String path, boolean expectedFound, String... strings) {
if (expectedFound) {
@ -499,97 +503,81 @@ public abstract class JavadocTester {
* Checks for content in (or not in) the generated output.
* Within the search strings, the newline character \n
* will be translated to the platform newline character sequence.
* @param path a path within the most recent output directory, identifying
* where to look for the search strings.
*
* @param path a path within the most recent output directory, identifying
* where to look for the search strings.
* @param expectedFound true if all of the search strings are expected
* to be found, or false if all of the strings are expected to be
* not found
* @param strings the strings to be searched for
* to be found, or false if all of the strings are expected to be
* not found
* @param strings the strings to be searched for
*
* @see OutputChecker#check(String...)
*/
public void checkOutput(String path, boolean expectedFound, String... strings) {
// Read contents of file
try {
String fileString = readFile(outputDir, Path.of(path));
checkOutput(outputDir.resolve(path).toString(), fileString, expectedFound, strings);
} catch (Error e) {
checking("Read file");
failed("Error reading file: " + e);
}
new OutputChecker(path)
.setExpectFound(expectedFound)
.setExpectOrdered(false) // TODO, fix tests (32 failures) and change to true
.check(strings);
}
/**
* Checks for content in (or not in) the one of the output streams written by
* javadoc. Within the search strings, the newline character \n
* will be translated to the platform newline character sequence.
* @param output the output stream to check
*
* @param output the output stream to check
* @param expectedFound true if all of the search strings are expected
* to be found, or false if all of the strings are expected to be
* not found
* @param strings the strings to be searched for
* to be found, or false if all of the strings are expected to be
* not found
* @param strings the strings to be searched for
*
* @see OutputChecker#check(String...)
*/
public void checkOutput(Output output, boolean expectedFound, String... strings) {
checkOutput(output.toString(), outputMap.get(output), expectedFound, strings);
}
// NOTE: path may be the name of an Output stream as well as a file path
private void checkOutput(String path, String fileString, boolean expectedFound, String... strings) {
for (String stringToFind : strings) {
// log.logCheckOutput(path, expectedFound, stringToFind);
checking("checkOutput");
// Find string in file's contents
boolean isFound = findString(fileString, stringToFind);
if (isFound == expectedFound) {
passed(path + ": following text " + (isFound ? "found:" : "not found:") + "\n"
+ stringToFind);
} else {
failed(path + ": following text " + (isFound ? "found:" : "not found:") + "\n"
+ stringToFind + '\n' +
"found \n" +
fileString);
}
}
new OutputChecker(output)
.setExpectFound(expectedFound)
.setExpectOrdered(false) // TODO, fix tests (6 failures) and change to true
.check(strings);
}
/**
* Checks that there are no duplicate lines in one of the streams written by javadoc.
*
* @param output the output stream to check
*
* @see OutputChecker#checkUnique()
*/
public void checkUnique(Output output) {
checkUnique(output, ".*", true);
}
/**
* Checks that there are no duplicate lines that either match or don't match a given patter,
* Checks that there are no duplicate lines that either match or don't match a given pattern,
* in one of the streams written by javadoc.
* @param output the output stream to check
*
* @param output the output stream to check
* @param pattern a pattern to filter the lines to be checked
* @param select if {@code true}, lines that match the pattern will be checked for uniqueness;
* if {@code false}, lines that do not match the pattern will be checked
* @param select if {@code true}, lines that match the pattern will be checked for uniqueness;
* if {@code false}, lines that do not match the pattern will be checked
*
* @see OutputChecker#checkUnique(Pattern, boolean)
*/
public void checkUnique(Output output, String pattern, boolean select) {
checking("checkUnique");
Pattern filter = Pattern.compile(pattern);
Matcher m = filter.matcher("");
Map<String, Integer> linesSofar = new HashMap<>();
int lineNumber = 0;
int duplicates = 0;
for (String line : getOutputLines(output)) {
m.reset(line);
if (m.find() == select) {
Integer prev = linesSofar.putIfAbsent(line, ++lineNumber);
if (prev != null) {
out.println("duplicate line detected on line " + lineNumber
+ "; first occurrence on line " + prev);
out.println("line: " + line);
duplicates++;
}
}
}
if (duplicates == 0) {
passed("All lines are unique");
} else {
failed(duplicates + " duplicate lines found");
}
new OutputChecker(output).checkUnique(Pattern.compile(pattern), select);
}
/**
* Ensures that a series of strings appear only once, in the generated output.
* Note: this test does not exhaustively check for all other possible
* duplicates once one is found.
*
* @param path the file to check
* @param strings the strings
*
* @see OutputChecker#checkUnique(String...)
*/
public void checkUnique(String path, String... strings) {
new OutputChecker(path).checkUnique(strings);
}
/**
@ -658,7 +646,8 @@ public abstract class JavadocTester {
}
/**
* Gets the content of the one of the output streams written by javadoc.
* Returns the content of one of the output streams written by javadoc.
*
* @param output the name of the output stream
* @return the content of the output stream
*/
@ -667,7 +656,8 @@ public abstract class JavadocTester {
}
/**
* Gets the content of the one of the output streams written by javadoc.
* Returns the content of one of the output streams written by javadoc.
*
* @param output the name of the output stream
* @return the content of the output stream, as a line of lines
*/
@ -678,22 +668,23 @@ public abstract class JavadocTester {
/**
* Checks for files in (or not in) the generated output.
*
* @param expectedFound true if all of the files are expected
* to be found, or false if all of the files are expected to be
* not found
* @param paths the files to check, within the most recent output directory.
* */
* to be found, or false if all of the files are expected to be
* not found
* @param paths the files to check, within the most recent output directory.
*/
public void checkFiles(boolean expectedFound, String... paths) {
checkFiles(expectedFound, Arrays.asList(paths));
}
/**
* Checks for files in (or not in) the generated output.
* @param expectedFound true if all of the files are expected
* to be found, or false if all of the files are expected to be
* not found
* @param paths the files to check, within the most recent output directory.
* */
*
* @param expectedFound true if all of the files are expected to be found,
* or false if all of the files are expected to be not found
* @param paths the files to check, within the most recent output directory.
*/
public void checkFiles(boolean expectedFound, Collection<String> paths) {
for (String path: paths) {
// log.logCheckFile(path, expectedFound);
@ -711,54 +702,16 @@ public abstract class JavadocTester {
/**
* Checks that a series of strings are found in order in a file in
* the generated output.
* @param path the file to check
* @param strings the strings whose order to check
*
* @param path the file to check
* @param strings the strings whose order to check
*
* @see OutputChecker#check(String...)
*/
public void checkOrder(String path, String... strings) {
Path file = outputDir.resolve(path);
String fileString = readOutputFile(path);
int prevIndex = -1;
for (String s : strings) {
s = s.replace("\n", NL); // normalize new lines
int currentIndex = fileString.indexOf(s, prevIndex + 1);
checking("file: " + file + ": " + s + " at index " + currentIndex);
if (currentIndex == -1) {
failed(file, s + " not found.");
continue;
}
if (currentIndex > prevIndex) {
passed(file, s + " is in the correct order");
} else {
failed(file, s + " is in the wrong order.");
}
prevIndex = currentIndex;
}
}
/**
* Ensures that a series of strings appear only once, in the generated output,
* noting that, this test does not exhaustively check for all other possible
* duplicates once one is found.
* @param path the file to check
* @param strings ensure each are unique
*/
public void checkUnique(String path, String... strings) {
Path file = outputDir.resolve(path);
String fileString = readOutputFile(path);
for (String s : strings) {
int currentIndex = fileString.indexOf(s);
checking(s + " at index " + currentIndex);
if (currentIndex == -1) {
failed(file, s + " not found.");
continue;
}
int nextindex = fileString.indexOf(s, currentIndex + s.length());
if (nextindex == -1) {
passed(file, s + " is unique");
} else {
failed(file, s + " is not unique, found at " + nextindex);
}
}
new OutputChecker(path)
.setExpectOrdered(true) // be explicit
.check(strings);
}
/**
@ -766,7 +719,7 @@ public abstract class JavadocTester {
*
* @param baseDir1 the directory containing the first set of files
* @param baseDir2 the directory containing the second set of files
* @param files the set of files to be compared
* @param files the set of files to be compared
*/
public void diff(String baseDir1, String baseDir2, String... files) {
Path bd1 = Path.of(baseDir1);
@ -830,10 +783,10 @@ public abstract class JavadocTester {
content = new String(Files.readAllBytes(file), charset);
fileContentCache.put(file, new SoftReference<>(content));
return content;
} catch (FileNotFoundException e) {
throw new Error("File not found: " + fileName + ": " + e);
} catch (FileNotFoundException | NoSuchFileException e) {
throw new Error("File not found: " + fileName + ": " + e, e);
} catch (IOException e) {
throw new Error("Error reading file: " + fileName + ": " + e);
throw new Error("Error reading file: " + fileName + ": " + e, e);
}
}
@ -941,22 +894,6 @@ public abstract class JavadocTester {
}
}
/**
* Searches for the string in the given file and return true
* if the string was found.
*
* @param fileString the contents of the file to search through
* @param stringToFind the string to search for
* @return true if the string was found
*/
private boolean findString(String fileString, String stringToFind) {
// javadoc (should) always use the platform newline sequence,
// but in the strings to find it is more convenient to use the Java
// newline character. So we translate \n to NL before we search.
stringToFind = stringToFind.replace("\n", NL);
return fileString.contains(stringToFind);
}
/**
* Compares the two given files.
*
@ -975,6 +912,459 @@ public abstract class JavadocTester {
}
}
/**
* A flexible checker for checking the content of generated files and output streams.
*
* Configuration can be done with a series of chained method calls.
* Checks can be specified as either literal strings or regular expressions.
*/
public class OutputChecker {
private final String name;
private final String content;
private boolean allowOverlaps = false;
private boolean expectFound = true;
private boolean expectOrdered = true;
private List<Range> matches = new ArrayList<>();
private Range lastMatch;
private enum SearchKind {
TEXT, PATTERN;
@Override
public String toString() {
return name().toLowerCase(Locale.ROOT);
}
}
/** A half-open interval {@code [start, end)} to record the position of a match. */
record Range(int start, int end) {
static Range of(int start, int end) {
return new Range(start, end);
}
boolean overlaps(Range other) {
// Intervals do not overlap if one interval is completely before or completely after the other:
// that is, other.end <= start || end <= other.start
// Invert that for when intervals do overlap, and simplify to the following expression:
return other.end > start && end > other.start;
}
String toIntervalString() {
return "[" + start + "," + end + ")";
}
}
/**
* Creates an output checker for a file written by the most recent run of javadoc.
* If the file cannot be found or there is any other error while reading the file,
* an error will be reported and all subsequent {@code check...} methods will be skipped
*
* @param file the file
*/
public OutputChecker(String file) {
String c = null;
try {
c = readFile(file);
} catch (Error e) {
JavadocTester.this.checking("Read file " + file);
if (e.getCause() instanceof IOException) {
// exception probably thrown (with known message) by readFile
failed(e.getMessage());
} else {
failed("Error reading file: " + e);
}
}
if (c == null) {
name = null;
content = null;
} else {
name = file;
content = c;
}
}
/**
* Creates an output checker for an output stream written by the most recent run of javadoc.
*
* @param output the output
*/
public OutputChecker(Output output) {
name = output.name();
content = getOutput(output);
}
/**
* Specifies whether matches are expected to be found or not.
* The default is {@code true}.
*
* @param expectFound whether matches are expected to be found
* @return this object
*/
public OutputChecker setExpectFound(boolean expectFound) {
this.expectFound = expectFound;
return this;
}
/**
* Specifies whether matches are expected to be found in order or not.
* The default is {@code true}.
*
* @param expectOrdered whether matches should be ordered
* @return this object
*/
public OutputChecker setExpectOrdered(boolean expectOrdered) {
this.expectOrdered = expectOrdered;
return this;
}
/**
* Specifies whether matches are allowed to overlap.
* The default is {@code false}.
*
* @param allowOverlaps whether matches may overlap
* @return this object
*/
public OutputChecker setAllowOverlaps(boolean allowOverlaps) {
this.allowOverlaps = allowOverlaps;
return this;
}
/**
* Checks for the presence (or absence) of a series of strings.
* Within the search strings, the newline character {@code \n}
* will be translated to the platform newline character sequence.
*
* @param strings the strings to be searched for
*/
public OutputChecker check(String... strings) {
if (name == null) {
out.println("Skipping checks for:" + NL
+ List.of(strings).stream()
.map(s -> " " + toShortString(s))
.collect(Collectors.joining(NL)));
return this;
}
for (String stringToFind : strings) {
check(startPos -> findString(stringToFind, startPos), SearchKind.TEXT, stringToFind);
}
return this;
}
/**
* Checks for the presence (or absence) of a series of regular expressions.
* Unlike {@link #check(String...)}, there is no special handling for
* newline characters. Use {@code \R} to match the platform newline sequence.
*
* @param patterns the regular expressions to be searched for
*/
public OutputChecker check(Pattern... patterns) {
if (name == null) {
out.println("Skipping checks for:" + NL
+ List.of(patterns).stream()
.map(p -> " " + toShortString(p.pattern()))
.collect(Collectors.joining(NL)));
return this;
}
for (Pattern pattern : patterns) {
check(startPos -> findPattern(pattern, startPos), SearchKind.PATTERN, pattern.pattern());
}
return this;
}
/**
* Checks for the presence (or absence) of an item.
*
* @param finder a function to find the next occurrence of an item starting at a given position
* @param kind the kind of the item ({@code "text"} or {@code "pattern:} to include in messages
* @param s a string for the item, to be included in messages
*/
private void check(Function<Integer, Range> finder, SearchKind kind, String s) {
checking("checkOutput", kind);
int start = getStart();
Range r = finder.apply(start);
boolean isFound = r != null;
if (isFound == expectFound) {
matches.add(lastMatch = r);
passed(name + ": following " + kind + " " + (isFound ? "found:" : "not found:") + "\n"
+ s);
} else {
// item not found in order, so check if the item is found out of order, to determine the best message
if (expectFound && expectOrdered && start > 0) {
Range r2 = finder.apply(0);
if (r2 != null) {
failed(name + ": following " + kind + " was found on line "
+ getLineNumber(r2.start)
+ ", but not in order as expected, on or after line "
+ getLineNumber(start)
+ ":\n"
+ s);
return;
}
}
failed(name + ": following " + kind + " "
+ (isFound ? "found:" : "not found:") + "\n"
+ s + '\n' + "found \n" + content);
}
}
/**
* Checks that there are no duplicate lines in the content.
*/
public OutputChecker checkUnique() {
checkUnique(Pattern.compile(".*"), true);
return this;
}
/**
* Checks that there are no duplicate lines that either match or don't match a given pattern,
* in one of the streams written by javadoc.
*
* @param pattern a pattern to filter the lines to be checked
* @param select if {@code true}, lines that match the pattern will be checked for uniqueness;
* if {@code false}, lines that do not match the pattern will be checked
*/
public OutputChecker checkUnique(Pattern pattern, boolean select ) {
if (name == null) {
out.println("Skipping checkUnique");
return this;
}
checking("checkUnique", SearchKind.PATTERN);
Matcher m = pattern.matcher("");
Map<String, Integer> linesSofar = new HashMap<>();
int lineNumber = 0;
int duplicates = 0;
for (String line : content.split(NL)) {
m.reset(line);
if (m.find() == select) {
Integer prev = linesSofar.putIfAbsent(line, ++lineNumber);
if (prev != null) {
out.println("duplicate line detected on line " + lineNumber
+ "; first occurrence on line " + prev);
out.println("line: " + line);
duplicates++;
}
}
}
if (duplicates == 0) {
passed("All lines are unique");
} else {
failed(duplicates + " duplicate lines found");
}
return this;
}
/**
* Checks that each of a series of strings appears only once in the generated output.
* Note: this test does not exhaustively check for all other possible duplicates once one is found.
*
* @param strings the strings
*/
public OutputChecker checkUnique(String... strings) {
return checkUnique(SearchKind.TEXT, List.of(strings), this::findString);
}
/**
* Checks that each of a series of pattern matches appears only once in the generated output.
* Note: this test does not exhaustively check for all other possible duplicates once one is found.
*
* @param patterns the patterns
*/
public OutputChecker checkUnique(Pattern... patterns) {
return checkUnique(SearchKind.PATTERN, List.of(patterns), this::findPattern);
}
private <T> OutputChecker checkUnique(SearchKind kind, List<T> items, BiFunction<T, Integer, Range> finder) {
if (name == null) {
out.println("Skipping checkUnique");
return this;
}
Range latest = null;
for (T item : items) {
int start = getStart();
Range r = finder.apply(item, start);
checking("checkUnique at index " + start, SearchKind.TEXT);
if (r == null) {
failed(name + ": " + item + " not found.");
continue;
}
// only update lastMatch for the initial match of each item
if (lastMatch == null) {
lastMatch = r;
}
Range next = finder.apply(item, r.end);
if (next == null) {
passed(name + ": " + item + " is unique");
} else {
failed(name + ": " + item + " is not unique, found at " + next.start);
}
}
if (latest != null) {
lastMatch = latest;
}
return this;
}
/**
* Checks that all the output has been matched by preceding checks with this object.
* It does not matter whether the checks were ordered or not.
* The results of the matches are sorted and then checked to be adjacent and to
* cover the entire content.
*
* @apiNote This is probably most useful for checking diagnostic output,
* in which case care must be taken to allow for platform differences
* in the output, such as file separators and newline sequences.
*/
public OutputChecker checkComplete() {
if (name == null) {
out.println("Skipping checkComplete");
return this;
}
JavadocTester.this.checking("checking for complete coverage of output");
List<Range> uncovered = new ArrayList<>();
List<Range> list = new ArrayList<>(matches);
list.sort(Comparator.comparing(Range::start));
int prev = 0;
for (Range r : list) {
if (r.start != prev) {
uncovered.add(new Range(prev, r.start));
}
prev = r.end;
}
if (prev != content.length()) {
uncovered.add(new Range(prev, content.length()));
}
if (uncovered.isEmpty()) {
passed("All output matched");
} else {
failed("The following output was not matched: "
+ uncovered.stream()
.map(Range::toIntervalString)
.collect(Collectors.joining(", ")));
}
return this;
}
/**
* Checks that no output is present.
*/
public OutputChecker checkEmpty() {
if (name == null) {
out.println("Skipping checkEmpty");
return this;
}
JavadocTester.this.checking("empty");
if (content == null || content.isEmpty()) {
passed(name + " is empty, as expected");
} else {
failed(name + " is not empty; contains:\n"
+ content);
}
return this;
}
/**
* Checks that at least of a set of alternatives is found.
*/
public OutputChecker checkAnyOf(String... strings) {
return checkAnyOf(SearchKind.TEXT, List.of(strings), this::findString);
}
/**
* Checks that at least of a set of alternatives is found.
*/
public OutputChecker checkAnyOf(Pattern... patterns) {
return checkAnyOf(SearchKind.PATTERN, List.of(patterns), this::findPattern);
}
/**
* Checks that at least of a set of alternatives is found.
*
*/
private <T> OutputChecker checkAnyOf(SearchKind kind, List<T> items, BiFunction<T, Integer, Range> finder) {
if (name == null) {
out.println("Skipping checkAnyOf");
return this;
}
checking("checkAnyOf", kind);
Range earliest = null;
int start = getStart();
int count = 0;
for (T item : items) {
Range r = finder.apply(item, start);
if (r != null) {
count++;
if (earliest == null || rangeComparator.compare(earliest, r) > 0) {
earliest = r;
}
}
}
if (earliest != null) {
lastMatch = earliest;
}
if (count == 0) {
failed("no match found for any " + kind);
} else {
passed(count + " matches found; earliest is " + earliest.toIntervalString());
}
return this;
}
Comparator<Range> rangeComparator = Comparator.comparing(Range::start).thenComparing(Range::end);
private void checking(String name, SearchKind kind) {
JavadocTester.this.checking(name + " " + kind.name()
+ " allowOverlaps:" + allowOverlaps
+ " expectFound:" + expectFound
+ " expectOrdered:" + expectOrdered);
}
private Range findString(String stringToFind, int start) {
// javadoc (should) always use the platform newline sequence,
// but in the strings to find it is more convenient to use the Java
// newline character. So we translate \n to NL before we search.
stringToFind = stringToFind.replace("\n", NL);
int i = content.indexOf(stringToFind, start);
return i >= 0 ? Range.of(i, i + stringToFind.length()) : null;
}
private Range findPattern(Pattern p, int start) {
Matcher m = p.matcher(content);
return m.find(start) ? Range.of(m.start(), m.end()) : null;
}
private int getStart() {
if (lastMatch == null || !expectOrdered) {
return 0;
}
return allowOverlaps ? lastMatch.start + 1 : lastMatch.end;
}
private int getLineNumber(int pos) {
Pattern p = Pattern.compile("\\R");
Matcher m = p.matcher(content);
int line = 1;
int start = 0;
while (m.find(start) && m.start() < pos) {
line++;
start = m.start() + 1;
}
return line;
}
private String toShortString(String s) {
final int MAX = 64;
s = s.replaceAll("\\s+", " ");
if (s.length() > MAX) {
s = s.substring(0, MAX / 2 - 2) + " ... " + s.substring(s.length() - MAX / 2 - 2);
}
return s;
}
}
/**
* Utility class to simplify the handling of temporarily setting a
* new stream for System.out or System.err.

@ -0,0 +1,438 @@
/*
* Copyright (c) 2021, 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.
*/
/*
* @test
* @bug 8273154
* @summary Provide a JavadocTester method for non-overlapping, unordered output matching
* @library /tools/lib/ ../lib
* @modules jdk.javadoc/jdk.javadoc.internal.tool
* @build toolbox.ToolBox javadoc.tester.*
* @run main TestJavadocTester
*/
import javadoc.tester.JavadocTester;
import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import toolbox.ToolBox;
/**
* Tests basic mechanisms in the {@code JavadocTester} class.
*
* It is not a direct test of the javadoc tool or the output generated by the
* Standard Doclet, although both are indirectly used as part of this test.
*
* The test works by exercising the {@code JavadocTester} API with a series of
* positive and negative tests. The {@code passed} and {@code failed} methods
* are overridden to record the messages reported by the underlying instance, so
* that the messages can subsequently be verified. Also, {@code printSummary}
* is overridden to suppress the default action to throw {@code Error} when
* tests have failed.
*/
public class TestJavadocTester extends JavadocTester {
public static void main(String... args) throws Exception {
TestJavadocTester tester = new TestJavadocTester();
tester.setup().runTests();
}
private final List<String> messages = new ArrayList<>();
private int testErrors = 0;
/**
* Overrides the default implementation of {@code passed} to record the argument.
* {@inheritDoc}
*
* @param message a short description of the outcome
*/
@Override
public void passed(String message) {
super.passed(message);
messages.add("Passed: " + message);
}
/**
* Overrides the default implementation of {@code failed} to record the argument.
* {@inheritDoc}
*
* @param message a short description of the outcome
*/
@Override
public void failed(String message) {
super.failed(message);
messages.add("FAILED: " + message);
}
/**
* Overrides the default implementation of {@code printSummary} to suppress
* the error thrown as a result of errors reported by {@code JavadocTester}.
* Instead, an error is thrown if any errors are found by the tests in this class.
*/
@Override
public void printSummary() {
try {
super.printSummary();
} catch (Error e) {
if (e.getClass() != Error.class) {
throw e;
}
report("Suppressed: " + e);
}
if (testErrors > 0) {
report(testErrors + " errors found");
throw new Error(testErrors + " errors found");
}
}
/**
* Checks the content of messages reported by the {@code passed} and {@code failed}
* methods in {@code JavadocTester}. The messages are saved by the local overloads
* of those methods in this class.
*
* Because some of the messages are <em>very</em> long, it is enough to pass in
* initial substrings of the expected messages.
*
* Note that messages reported by {@code JavadocTester} use filenames as given
* to the various {@code check...} calls. By convention, these always use {@code /}
* as the file separator, and not the platform file separator.
*
* @param expect initial substrings of expected messages
*/
void checkMessages(String... expect) {
for (String e : expect) {
Optional<String> match = messages.stream()
.filter(m -> m.startsWith(e))
.findFirst();
if (match.isPresent()) {
report("found '" + e + "'");
} else {
report("ERROR: no message found for '" + e + "'");
testErrors++;
}
}
}
/**
* Reports a message, preceded by {@code >>> }.
*
* It is helpful/important to distinguish the messages written as a side-effect
* of the underlying tests from the messages used to report the outcome of the
* tests that verify those messages. Instead of interposing to mark the messages
* written as a side effect of the underlying tests, we leave those messages
* unchanged, and instead, mark the messages reporting whether those messages
* are as expected or not.
*
* @param message the message to be reported.
*/
private void report(String message) {
message.lines().forEachOrdered(l -> out.println(">>> " + l));
}
//-------------------------------------------------
private final ToolBox tb = new ToolBox();
TestJavadocTester setup() throws IOException {
Path src = Path.of("src");
tb.writeJavaFiles(src, """
package p;
/**
* First sentence abc.
* Second sentence.
* abc123
* def456
* ghi789
* abc123
* def456
* ghi789
*/
public class C {
private C() { }
/** m3 comment. */
public void m3() { }
/** m2 comment. */
public void m2() { }
/** m1 comment. */
public void m1() { }
}
""");
javadoc("-d", "out",
"-sourcepath", src.toString(),
"-noindex", "-nohelp",
"p");
return this;
}
@Test
public void testSimpleStringCheck() {
messages.clear();
new OutputChecker("p/C.html")
.check("Second sentence",
"abc123",
"def456");
messages.forEach(this::report);
checkMessages(
"""
Passed: p/C.html: following text found:
Second sentence""",
"""
Passed: p/C.html: following text found:
abc123""",
"""
Passed: p/C.html: following text found:
def456""");
}
@Test
public void testSimpleNegativeStringCheck_expected() {
messages.clear();
new OutputChecker("p/C.html")
.setExpectFound(false)
.check("Third sentence.");
checkMessages(
"""
Passed: p/C.html: following text not found:
Third sentence""");
}
@Test
public void testSimpleNegativeStringCheck_unexpected() {
messages.clear();
new OutputChecker("p/C.html")
.check("Third sentence.");
checkMessages(
"""
FAILED: p/C.html: following text not found:
Third sentence""");
}
@Test
public void testSimpleRegexCheck() {
messages.clear();
new OutputChecker("p/C.html")
.check(Pattern.compile("S.cond s.nt.nc."),
Pattern.compile("[abc]{3}[123]{3}"),
Pattern.compile("d.f4.6"));
checkMessages(
"""
Passed: p/C.html: following pattern found:
S.cond s.nt.nc.""",
"""
Passed: p/C.html: following pattern found:
[abc]{3}[123]{3}""",
"""
Passed: p/C.html: following pattern found:
d.f4.6""");
}
@Test
public void testOrdered() {
messages.clear();
// methods are listed alphabetically in the Summary table,
// but in source-code order in the Details section.
new OutputChecker("p/C.html")
.check("<h2>Method Summary</h2>",
"<a href=\"#m1()\" class=\"member-name-link\">m1</a>",
"<a href=\"#m2()\" class=\"member-name-link\">m2</a>",
"<a href=\"#m3()\" class=\"member-name-link\">m3</a>")
.check("<h2>Method Details</h2>",
"<section class=\"detail\" id=\"m3()\">\n",
"<section class=\"detail\" id=\"m2()\">\n",
"<section class=\"detail\" id=\"m1()\">\n");
checkMessages(
"""
Passed: p/C.html: following text found:
<h2>Method Summary</h2>""",
"""
Passed: p/C.html: following text found:
<a href="#m1()" class="member-name-link">m1</a>""",
"""
Passed: p/C.html: following text found:
<a href="#m2()" class="member-name-link">m2</a>""",
"""
Passed: p/C.html: following text found:
<a href="#m3()" class="member-name-link">m3</a>""",
"""
Passed: p/C.html: following text found:
<h2>Method Details</h2>""",
"""
Passed: p/C.html: following text found:
<section class="detail" id="m3()">""",
"""
Passed: p/C.html: following text found:
<section class="detail" id="m2()">""",
"""
Passed: p/C.html: following text found:
<section class="detail" id="m1()">"""
);
}
@Test
public void testUnordered_expected() {
messages.clear();
new OutputChecker("p/C.html")
.setExpectOrdered(false)
.check("Second sentence",
"First sentence");
checkMessages(
"""
Passed: p/C.html: following text found:
Second sentence""",
"""
Passed: p/C.html: following text found:
First sentence""");
}
@Test
public void testUnordered_unexpected() {
messages.clear();
new OutputChecker("p/C.html")
.check("Second sentence",
"First sentence");
checkMessages(
"""
Passed: p/C.html: following text found:
Second sentence""",
"""
FAILED: p/C.html: following text was found on line""");
}
@Test
public void testComplete_Ordered() {
messages.clear();
// In the following calls, the strings are specified in the expected order.
// File separators are made platform-specific by calling 'fix'.
// Newlines are handled automatically by the 'check' method.
new OutputChecker(Output.OUT)
.check("Loading source files for package p...\n",
"Constructing Javadoc information...\n",
fix("Creating destination directory: \"out/\"\n"))
.check(Pattern.compile("Standard Doclet .*\\R"))
.check("Building tree for all the packages and classes...\n",
fix("Generating out/p/C.html...\n"),
fix("Generating out/p/package-summary.html...\n"),
fix("Generating out/p/package-tree.html...\n"),
fix("Generating out/overview-tree.html...\n"),
fix("Generating out/index.html...\n"))
.checkComplete();
checkMessages("Passed: All output matched");
}
@Test
public void testComplete_Unordered() {
messages.clear();
// In the following calls, the strings are deliberately specified out of the expected order.
// File separators are made platform-specific by calling 'fix'.
// Newlines are handled automatically by the 'check' method.
new OutputChecker(Output.OUT)
.setExpectOrdered(false)
.check("Loading source files for package p...\n",
"Constructing Javadoc information...\n",
"Building tree for all the packages and classes...\n")
.check(fix("Creating destination directory: \"out/\"\n",
"Generating out/index.html...\n",
"Generating out/overview-tree.html...\n",
"Generating out/p/package-tree.html...\n",
"Generating out/p/package-summary.html...\n",
"Generating out/p/C.html...\n"))
.check(Pattern.compile("Standard Doclet .*\\R"))
.checkComplete();
checkMessages("Passed: All output matched");
}
@Test
public void testEmpty() {
messages.clear();
new OutputChecker(Output.STDERR)
.checkEmpty();
checkMessages("Passed: STDERR is empty, as expected");
}
@Test
public void testBadFile() {
messages.clear();
new OutputChecker("does-not-exist.html")
.check("abcdef",
"very long string ".repeat(10))
.check(Pattern.quote("abcdef"),
Pattern.quote("very long string".repeat(10)));
checkMessages("FAILED: File not found: does-not-exist.html");
}
@Test
public void testAnyOf() {
messages.clear();
new OutputChecker("p/C.html")
.checkAnyOf("m1()", "m2()", "m3()") // expect all found
.checkAnyOf("m1()", "m2()", "M3()") // expect some found
.checkAnyOf("M1()", "M2()", "M3()"); // expect none found
checkMessages("Passed: 3 matches found",
"Passed: 2 matches found",
"FAILED: no match found for any text");
}
@Test
public void testUnique() {
messages.clear();
new OutputChecker("p/C.html")
.setExpectOrdered(false)
.checkUnique("id=\"m1()\"", "id=\"m2()\"", "id=\"m3()\"") // expect unique
.checkUnique("m1()", "m2()", "m3()"); // expect not unique
checkMessages("Passed: p/C.html: id=\"m1()\" is unique",
"Passed: p/C.html: id=\"m2()\" is unique",
"Passed: p/C.html: id=\"m3()\" is unique",
"FAILED: p/C.html: m1() is not unique",
"FAILED: p/C.html: m2() is not unique",
"FAILED: p/C.html: m3() is not unique");
}
/**
* {@return a string with {@code /} replaced by the platform file separator}
*
* @param item the string
*/
private String fix(String item) {
return item.replace("/", FS);
}
/**
* {@return an array of strings with {@code /} replaced by the platform file separator}
*
* @param items the strings
*/
private String[] fix(String... items) {
return Stream.of(items)
.map(this::fix)
.toArray(String[]::new);
}
}