1 /*
   2  * Copyright (c) 2002, 2019, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.
   8  *
   9  * This code is distributed in the hope that it will be useful, but WITHOUT
  10  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  11  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  12  * version 2 for more details (a copy is included in the LICENSE file that
  13  * accompanied this code).
  14  *
  15  * You should have received a copy of the GNU General Public License version
  16  * 2 along with this work; if not, write to the Free Software Foundation,
  17  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  18  *
  19  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  20  * or visit www.oracle.com if you need additional information or have any
  21  * questions.
  22  */
  23 
  24 package javadoc.tester;
  25 
  26 import java.io.BufferedWriter;
  27 import java.io.ByteArrayOutputStream;
  28 import java.io.File;
  29 import java.io.FileNotFoundException;
  30 import java.io.FileWriter;
  31 import java.io.FilenameFilter;
  32 import java.io.IOException;
  33 import java.io.PrintStream;
  34 import java.io.PrintWriter;
  35 import java.io.StringWriter;
  36 import java.lang.annotation.Annotation;
  37 import java.lang.annotation.Retention;
  38 import java.lang.annotation.RetentionPolicy;
  39 import java.lang.ref.SoftReference;
  40 import java.lang.reflect.InvocationTargetException;
  41 import java.lang.reflect.Method;
  42 import java.nio.charset.Charset;
  43 import java.nio.charset.UnsupportedCharsetException;
  44 import java.nio.file.Files;
  45 import java.nio.file.Path;
  46 import java.nio.file.Paths;
  47 import java.util.ArrayList;
  48 import java.util.Arrays;
  49 import java.util.Collection;
  50 import java.util.Collections;
  51 import java.util.EnumMap;
  52 import java.util.HashMap;
  53 import java.util.List;
  54 import java.util.Map;
  55 import java.util.Objects;
  56 import java.util.function.Function;
  57 
  58 
  59 /**
  60  * Test framework for running javadoc and performing tests on the resulting output.
  61  *
  62  * <p>
  63  * Tests are typically written as subtypes of JavadocTester, with a main
  64  * method that creates an instance of the test class and calls the runTests()
  65  * method. The runTests() methods calls all the test methods declared in the class,
  66  * and then calls a method to print a summary, and throw an exception if
  67  * any of the test methods reported a failure.
  68  *
  69  * <p>
  70  * Test methods are identified with a @Test annotation. They have no parameters.
  71  * The name of the method is not important, but if you have more than one, it is
  72  * recommended that the names be meaningful and suggestive of the test case
  73  * contained therein.
  74  *
  75  * <p>
  76  * Typically, a test method will invoke javadoc, and then perform various
  77  * checks on the results. The standard checks are:
  78  *
  79  * <dl>
  80  * <dt>checkExitCode
  81  * <dd>Check the exit code returned from javadoc.
  82  * <dt>checkOutput
  83  * <dd>Perform a series of checks on the contents on a file or output stream
  84  *     generated by javadoc.
  85  *     The checks can be either that a series of strings are found or are not found.
  86  * <dt>checkFiles
  87  * <dd>Perform a series of checks on the files generated by javadoc.
  88  *     The checks can be that a series of files are found or are not found.
  89  * </dl>
  90  *
  91  * <pre><code>
  92  *  public class MyTester extends JavadocTester {
  93  *      public static void main(String... args) throws Exception {
  94  *          MyTester tester = new MyTester();
  95  *          tester.runTests();
  96  *      }
  97  *
  98  *      // test methods...
  99  *      {@literal @}Test
 100  *      void test() {
 101  *          javadoc(<i>args</i>);
 102  *          checkExit(Exit.OK);
 103  *          checkOutput(<i>file</i>, true,
 104  *              <i>strings-to-find</i>);
 105  *          checkOutput(<i>file</i>, false,
 106  *              <i>strings-to-not-find</i>);
 107  *      }
 108  *  }
 109  * </code></pre>
 110  *
 111  * <p>
 112  * If javadoc is run more than once in a test method, you can compare the
 113  * results that are generated with the diff method. Since files written by
 114  * javadoc typically contain a timestamp, you may want to use the -notimestamp
 115  * option if you are going to compare the results from two runs of javadoc.
 116  *
 117  * <p>
 118  * If you have many calls of checkOutput that are very similar, you can write
 119  * your own check... method to reduce the amount of duplication. For example,
 120  * if you want to check that many files contain the same string, you could
 121  * write a method that takes a varargs list of files and calls checkOutput
 122  * on each file in turn with the string to be checked.
 123  *
 124  * <p>
 125  * You can also write you own custom check methods, which can use
 126  * readFile to get the contents of a file generated by javadoc,
 127  * and then use pass(...) or fail(...) to report whether the check
 128  * succeeded or not.
 129  *
 130  * <p>
 131  * You can have many separate test methods, each identified with a @Test
 132  * annotation. However, you should <b>not</b> assume they will be called
 133  * in the order declared in your source file.  If the order of a series
 134  * of javadoc invocations is important, do that within a single method.
 135  * If the invocations are independent, for better clarity, use separate
 136  * test methods, each with their own set of checks on the results.
 137  *
 138  * @author Doug Kramer
 139  * @author Jamie Ho
 140  * @author Jonathan Gibbons (rewrite)
 141  */
 142 public abstract class JavadocTester {
 143 
 144     public static final String FS = System.getProperty("file.separator");
 145     public static final String PS = System.getProperty("path.separator");
 146     public static final String NL = System.getProperty("line.separator");
 147     public static final Path currDir = Paths.get(".").toAbsolutePath().normalize();
 148 
 149     public enum Output {
 150         /** The name of the output stream from javadoc. */
 151         OUT,
 152         /** The name for any output written to System.out. */
 153         STDOUT,
 154         /** The name for any output written to System.err. */
 155         STDERR
 156     }
 157 
 158     /** The output directory used in the most recent call of javadoc. */
 159     protected File outputDir;
 160 
 161     /** The output charset used in the most recent call of javadoc. */
 162     protected Charset charset = Charset.defaultCharset();
 163 
 164     /** The exit code of the most recent call of javadoc. */
 165     private int exitCode;
 166 
 167     /** The output generated by javadoc to the various writers and streams. */
 168     private final Map<Output, String> outputMap = new EnumMap<>(Output.class);
 169 
 170     /** A cache of file content, to avoid reading files unnecessarily. */
 171     private final Map<File,SoftReference<String>> fileContentCache = new HashMap<>();
 172     /** The charset used for files in the fileContentCache. */
 173     private Charset fileContentCacheCharset = null;
 174 
 175     /** Stream used for logging messages. */
 176     protected final PrintStream out = System.out;
 177 
 178     /** The directory containing the source code for the test. */
 179     public static final String testSrc = System.getProperty("test.src");
 180 
 181     /**
 182      * Get the path for a source file in the test source directory.
 183      * @param path the path of a file or directory in the source directory
 184      * @return the full path of the specified file
 185      */
 186     public static String testSrc(String path) {
 187         return new File(testSrc, path).getPath();
 188     }
 189 
 190     /**
 191      * Alternatives for checking the contents of a directory.
 192      */
 193     public enum DirectoryCheck {
 194         /**
 195          * Check that the directory is empty.
 196          */
 197         EMPTY((file, name) -> true),
 198         /**
 199          * Check that the directory does not contain any HTML files,
 200          * such as may have been generated by a prior run of javadoc
 201          * using this directory.
 202          * For now, the check is only performed on the top level directory.
 203          */
 204         NO_HTML_FILES((file, name) -> name.endsWith(".html")),
 205         /**
 206          * No check is performed on the directory contents.
 207          */
 208         NONE(null) { @Override void check(File dir) { } };
 209 
 210         /** The filter used to detect that files should <i>not</i> be present. */
 211         FilenameFilter filter;
 212 
 213         DirectoryCheck(FilenameFilter f) {
 214             filter = f;
 215         }
 216 
 217         void check(File dir) {
 218             if (dir.isDirectory()) {
 219                 String[] contents = dir.list(filter);
 220                 if (contents == null)
 221                     throw new Error("cannot list directory: " + dir);
 222                 if (contents.length > 0) {
 223                     System.err.println("Found extraneous files in dir:" + dir.getAbsolutePath());
 224                     for (String x : contents) {
 225                         System.err.println(x);
 226                     }
 227                     throw new Error("directory has unexpected content: " + dir);
 228                 }
 229             }
 230         }
 231     }
 232 
 233     private DirectoryCheck outputDirectoryCheck = DirectoryCheck.EMPTY;
 234 
 235     private boolean automaticCheckAccessibility = true;
 236     private boolean automaticCheckLinks = true;
 237 
 238     /** The current subtest number. Incremented when checking(...) is called. */
 239     private int numTestsRun = 0;
 240 
 241     /** The number of subtests passed. Incremented when passed(...) is called. */
 242     private int numTestsPassed = 0;
 243 
 244     /** The current run of javadoc. Incremented when javadoc is called. */
 245     private int javadocRunNum = 0;
 246 
 247     /** The current subtest number for this run of javadoc. Incremented when checking(...) is called. */
 248     private int javadocTestNum = 0;
 249 
 250     /** Marker annotation for test methods to be invoked by runTests. */
 251     @Retention(RetentionPolicy.RUNTIME)
 252     public @interface Test { }
 253 
 254     /**
 255      * Run all methods annotated with @Test, followed by printSummary.
 256      * Typically called on a tester object in main()
 257      * @throws Exception if any errors occurred
 258      */
 259     public void runTests() throws Exception {
 260         runTests(m -> new Object[0]);
 261     }
 262 
 263     /**
 264      * Runs all methods annotated with @Test, followed by printSummary.
 265      * Typically called on a tester object in main()
 266      * @param f a function which will be used to provide arguments to each
 267      *          invoked method
 268      * @throws Exception if any errors occurred
 269      */
 270     public void runTests(Function<Method, Object[]> f) throws Exception {
 271         for (Method m: getClass().getDeclaredMethods()) {
 272             Annotation a = m.getAnnotation(Test.class);
 273             if (a != null) {
 274                 try {
 275                     out.println("Running test " + m.getName());
 276                     m.invoke(this, f.apply(m));
 277                 } catch (InvocationTargetException e) {
 278                     Throwable cause = e.getCause();
 279                     throw (cause instanceof Exception) ? ((Exception) cause) : e;
 280                 }
 281                 out.println();
 282             }
 283         }
 284         printSummary();
 285     }
 286 
 287     /**
 288      * Runs javadoc.
 289      * The output directory used by this call and the final exit code
 290      * will be saved for later use.
 291      * To aid the reader, it is recommended that calls to this method
 292      * put each option and the arguments it takes on a separate line.
 293      *
 294      * Example:
 295      * <pre><code>
 296      *  javadoc("-d", "out",
 297      *          "-sourcepath", testSrc,
 298      *          "-notimestamp",
 299      *          "pkg1", "pkg2", "pkg3/C.java");
 300      * </code></pre>
 301      *
 302      * @param args the arguments to pass to javadoc
 303      */
 304     public void javadoc(String... args) {
 305         outputMap.clear();
 306         fileContentCache.clear();
 307 
 308         javadocRunNum++;
 309         javadocTestNum = 0; // reset counter for this run of javadoc
 310         if (javadocRunNum == 1) {
 311             out.println("Running javadoc...");
 312         } else {
 313             out.println("Running javadoc (run "+ javadocRunNum + ")...");
 314         }
 315 
 316         outputDir = new File(".");
 317         String charsetArg = null;
 318         String docencodingArg = null;
 319         String encodingArg = null;
 320         for (int i = 0; i < args.length - 2; i++) {
 321             switch (args[i]) {
 322                 case "-d":
 323                     outputDir = new File(args[++i]);
 324                     break;
 325                 case "-charset":
 326                     charsetArg = args[++i];
 327                     break;
 328                 case "-docencoding":
 329                     docencodingArg = args[++i];
 330                     break;
 331                 case "-encoding":
 332                     encodingArg = args[++i];
 333                     break;
 334             }
 335         }
 336 
 337         // The following replicates HtmlConfiguration.finishOptionSettings0
 338         // and sets up the charset used to read files.
 339         String cs;
 340         if (docencodingArg == null) {
 341             if (charsetArg == null) {
 342                 cs = (encodingArg == null) ? "UTF-8" : encodingArg;
 343             } else {
 344                 cs = charsetArg;
 345             }
 346         } else {
 347            cs = docencodingArg;
 348         }
 349         try {
 350             charset = Charset.forName(cs);
 351         } catch (UnsupportedCharsetException e) {
 352             charset = Charset.defaultCharset();
 353         }
 354 
 355         out.println("args: " + Arrays.toString(args));
 356 //        log.setOutDir(outputDir);
 357 
 358         outputDirectoryCheck.check(outputDir);
 359 
 360         // This is the sole stream used by javadoc
 361         WriterOutput outOut = new WriterOutput();
 362 
 363         // These are to catch output to System.out and System.err,
 364         // in case these are used instead of the primary streams
 365         StreamOutput sysOut = new StreamOutput(System.out, System::setOut);
 366         StreamOutput sysErr = new StreamOutput(System.err, System::setErr);
 367 
 368         try {
 369             exitCode = jdk.javadoc.internal.tool.Main.execute(args, outOut.pw);
 370         } finally {
 371             outputMap.put(Output.STDOUT, sysOut.close());
 372             outputMap.put(Output.STDERR, sysErr.close());
 373             outputMap.put(Output.OUT, outOut.close());
 374         }
 375 
 376         outputMap.forEach((name, text) -> {
 377             if (!text.isEmpty()) {
 378                 out.println("javadoc " + name + ":");
 379                 out.println(text);
 380             }
 381         });
 382 
 383         if (exitCode == Exit.OK.code && outputDir.exists()) {
 384             if (automaticCheckLinks) {
 385                 checkLinks();
 386             }
 387             if (automaticCheckAccessibility) {
 388                 checkAccessibility();
 389             }
 390         }
 391     }
 392 
 393     /**
 394      * Sets the kind of check for the initial contents of the output directory
 395      * before javadoc is run.
 396      * The filter should return true for files that should <b>not</b> appear.
 397      * @param c the kind of check to perform
 398      */
 399     public void setOutputDirectoryCheck(DirectoryCheck c) {
 400         outputDirectoryCheck = c;
 401     }
 402 
 403     /**
 404      * Sets whether or not to perform an automatic call of checkAccessibility.
 405      */
 406     public void setAutomaticCheckAccessibility(boolean b) {
 407         automaticCheckAccessibility = b;
 408     }
 409 
 410     /**
 411      * Sets whether or not to perform an automatic call of checkLinks.
 412      */
 413     public void setAutomaticCheckLinks(boolean b) {
 414         automaticCheckLinks = b;
 415     }
 416 
 417     /**
 418      * The exit codes returned by the javadoc tool.
 419      * @see jdk.javadoc.internal.tool.Main.Result
 420      */
 421     public enum Exit {
 422         OK(0),        // Javadoc completed with no errors.
 423         ERROR(1),     // Completed but reported errors.
 424         CMDERR(2),    // Bad command-line arguments
 425         SYSERR(3),    // System error or resource exhaustion.
 426         ABNORMAL(4);  // Javadoc terminated abnormally
 427 
 428         Exit(int code) {
 429             this.code = code;
 430         }
 431 
 432         final int code;
 433 
 434         @Override
 435         public String toString() {
 436             return name() + '(' + code + ')';
 437         }
 438     }
 439 
 440     /**
 441      * Checks the exit code of the most recent call of javadoc.
 442      *
 443      * @param expected the exit code that is required for the test
 444      * to pass.
 445      */
 446     public void checkExit(Exit expected) {
 447         checking("check exit code");
 448         if (exitCode == expected.code) {
 449             passed("return code " + exitCode);
 450         } else {
 451             failed("return code " + exitCode +"; expected " + expected);
 452         }
 453     }
 454 
 455     /**
 456      * Checks for content in (or not in) the generated output.
 457      * Within the search strings, the newline character \n
 458      * will be translated to the platform newline character sequence.
 459      * @param path a path within the most recent output directory
 460      *  or the name of one of the output buffers, identifying
 461      *  where to look for the search strings.
 462      * @param expectedFound true if all of the search strings are expected
 463      *  to be found, or false if the file is not expected to be found
 464      * @param strings the strings to be searched for
 465      */
 466     public void checkFileAndOutput(String path, boolean expectedFound, String... strings) {
 467         if (expectedFound) {
 468             checkOutput(path, true, strings);
 469         } else {
 470             checkFiles(false, path);
 471         }
 472     }
 473 
 474     /**
 475      * Checks for content in (or not in) the generated output.
 476      * Within the search strings, the newline character \n
 477      * will be translated to the platform newline character sequence.
 478      * @param path a path within the most recent output directory, identifying
 479      *  where to look for the search strings.
 480      * @param expectedFound true if all of the search strings are expected
 481      *  to be found, or false if all of the strings are expected to be
 482      *  not found
 483      * @param strings the strings to be searched for
 484      */
 485     public void checkOutput(String path, boolean expectedFound, String... strings) {
 486         // Read contents of file
 487         try {
 488             String fileString = readFile(outputDir, path);
 489             checkOutput(new File(outputDir, path).getPath(), fileString, expectedFound, strings);
 490         } catch (Error e) {
 491             checking("Read file");
 492             failed("Error reading file: " + e);
 493         }
 494     }
 495 
 496     /**
 497      * Checks for content in (or not in) the one of the output streams written by
 498      * javadoc. Within the search strings, the newline character \n
 499      * will be translated to the platform newline character sequence.
 500      * @param output the output stream to check
 501      * @param expectedFound true if all of the search strings are expected
 502      *  to be found, or false if all of the strings are expected to be
 503      *  not found
 504      * @param strings the strings to be searched for
 505      */
 506     public void checkOutput(Output output, boolean expectedFound, String... strings) {
 507         checkOutput(output.toString(), outputMap.get(output), expectedFound, strings);
 508     }
 509 
 510     // NOTE: path may be the name of an Output stream as well as a file path
 511     private void checkOutput(String path, String fileString, boolean expectedFound, String... strings) {
 512         for (String stringToFind : strings) {
 513 //            log.logCheckOutput(path, expectedFound, stringToFind);
 514             checking("checkOutput");
 515             // Find string in file's contents
 516             boolean isFound = findString(fileString, stringToFind);
 517             if (isFound == expectedFound) {
 518                 passed(path + ": following text " + (isFound ? "found:" : "not found:") + "\n"
 519                         + stringToFind);
 520             } else {
 521                 failed(path + ": following text " + (isFound ? "found:" : "not found:") + "\n"
 522                         + stringToFind + '\n' +
 523                         "found \n" +
 524                         fileString);
 525             }
 526         }
 527     }
 528 
 529     /**
 530      * Performs some structural accessibility checks on the files generated by the most
 531      * recent run of javadoc.
 532      * The checks can be run automatically by calling {@link #setAutomaticCheckAccessibility}.
 533      */
 534     public void checkAccessibility() {
 535         checking("Check accessibility");
 536         A11yChecker c = new A11yChecker(out, this::readFile);
 537         try {
 538             c.checkDirectory(outputDir.toPath());
 539             c.report();
 540             int errors = c.getErrorCount();
 541             if (errors == 0) {
 542                 passed("No accessibility errors found");
 543             } else {
 544                 failed(errors + " errors found when checking accessibility");
 545             }
 546         } catch (IOException e) {
 547             failed("exception thrown when reading files: " + e);
 548         }
 549     }
 550 
 551     /**
 552      * Checks all the links within the files generated by the most
 553      * recent run of javadoc.
 554      * The checks can be run automatically by calling {@link #setAutomaticCheckLinks}.
 555      */
 556     public void checkLinks() {
 557         checking("Check links");
 558         LinkChecker c = new LinkChecker(out, this::readFile);
 559         try {
 560             c.checkDirectory(outputDir.toPath());
 561             c.report();
 562             int errors = c.getErrorCount();
 563             if (errors == 0) {
 564                 passed("Links are OK");
 565             } else {
 566                 failed(errors + " errors found when checking links");
 567             }
 568         } catch (IOException e) {
 569             failed("exception thrown when reading files: " + e);
 570         }
 571     }
 572 
 573     /**
 574      * Shows the heading structure for each of the specified files.
 575      * The structure is is printed in plain text to the main output stream.
 576      * No errors are reported (unless there is a problem reading a file)
 577      * but missing headings are noted within the output.
 578      * @params the files
 579      */
 580     public void showHeadings(String... paths) {
 581         ShowHeadings s = new ShowHeadings(out, this::readFile);
 582         for (String p : paths) {
 583             try {
 584                 File f = new File(outputDir, p);
 585                 s.checkFiles(List.of(f.toPath()), false, Collections.emptySet());
 586             } catch (IOException e) {
 587                 checking("Read file");
 588                 failed("Error reading file: " + e);
 589             }
 590         }
 591     }
 592 
 593     /**
 594      * Gets the content of the one of the output streams written by javadoc.
 595      * @param output the name of the output stream
 596      * @return the content of the output stream
 597      */
 598     public String getOutput(Output output) {
 599         return outputMap.get(output);
 600     }
 601 
 602     /**
 603      * Gets the content of the one of the output streams written by javadoc.
 604      * @param output the name of the output stream
 605      * @return the content of the output stream, as a line of lines
 606      */
 607     public List<String> getOutputLines(Output output) {
 608         String text = outputMap.get(output);
 609         return (text == null) ? Collections.emptyList() : Arrays.asList(text.split(NL));
 610     }
 611 
 612     /**
 613      * Checks for files in (or not in) the generated output.
 614      * @param expectedFound true if all of the files are expected
 615      *  to be found, or false if all of the files are expected to be
 616      *  not found
 617      * @param paths the files to check, within the most recent output directory.
 618      * */
 619     public void checkFiles(boolean expectedFound, String... paths) {
 620         checkFiles(expectedFound, Arrays.asList(paths));
 621     }
 622 
 623     /**
 624      * Checks for files in (or not in) the generated output.
 625      * @param expectedFound true if all of the files are expected
 626      *  to be found, or false if all of the files are expected to be
 627      *  not found
 628      * @param paths the files to check, within the most recent output directory.
 629      * */
 630     public void checkFiles(boolean expectedFound, Collection<String> paths) {
 631         for (String path: paths) {
 632 //            log.logCheckFile(path, expectedFound);
 633             checking("checkFile");
 634             File file = new File(outputDir, path);
 635             boolean isFound = file.exists();
 636             if (isFound == expectedFound) {
 637                 passed(file, "file " + (isFound ? "found:" : "not found:") + "\n");
 638             } else {
 639                 failed(file, "file " + (isFound ? "found:" : "not found:") + "\n");
 640             }
 641         }
 642     }
 643 
 644     /**
 645      * Checks that a series of strings are found in order in a file in
 646      * the generated output.
 647      * @param path the file to check
 648      * @param strings  the strings whose order to check
 649      */
 650     public void checkOrder(String path, String... strings) {
 651         File file = new File(outputDir, path);
 652         String fileString = readOutputFile(path);
 653         int prevIndex = -1;
 654         for (String s : strings) {
 655             s = s.replace("\n", NL); // normalize new lines
 656             int currentIndex = fileString.indexOf(s, prevIndex + 1);
 657             checking("file: " + file + ": " + s + " at index " + currentIndex);
 658             if (currentIndex == -1) {
 659                 failed(file, s + " not found.");
 660                 continue;
 661             }
 662             if (currentIndex > prevIndex) {
 663                 passed(file, s + " is in the correct order");
 664             } else {
 665                 failed(file, s + " is in the wrong order.");
 666             }
 667             prevIndex = currentIndex;
 668         }
 669     }
 670 
 671     /**
 672      * Ensures that a series of strings appear only once, in the generated output,
 673      * noting that, this test does not exhaustively check for all other possible
 674      * duplicates once one is found.
 675      * @param path the file to check
 676      * @param strings ensure each are unique
 677      */
 678     public void checkUnique(String path, String... strings) {
 679         File file = new File(outputDir, path);
 680         String fileString = readOutputFile(path);
 681         for (String s : strings) {
 682             int currentIndex = fileString.indexOf(s);
 683             checking(s + " at index " + currentIndex);
 684             if (currentIndex == -1) {
 685                 failed(file, s + " not found.");
 686                 continue;
 687             }
 688             int nextindex = fileString.indexOf(s, currentIndex + s.length());
 689             if (nextindex == -1) {
 690                 passed(file, s + " is unique");
 691             } else {
 692                 failed(file, s + " is not unique, found at " + nextindex);
 693             }
 694         }
 695     }
 696 
 697     /**
 698      * Compares a set of files in each of two directories.
 699      *
 700      * @param baseDir1 the directory containing the first set of files
 701      * @param baseDir2 the directory containing the second set of files
 702      * @param files the set of files to be compared
 703      */
 704     public void diff(String baseDir1, String baseDir2, String... files) {
 705         File bd1 = new File(baseDir1);
 706         File bd2 = new File(baseDir2);
 707         for (String file : files) {
 708             diff(bd1, bd2, file);
 709         }
 710     }
 711 
 712     /**
 713      * Copies a directory from one place to another.
 714      *
 715      * @param targetDir the directory to copy.
 716      * @param destDir the destination to copy the directory to.
 717      */
 718     // TODO: convert to using java.nio.Files.walkFileTree
 719     public void copyDir(String targetDir, String destDir) {
 720         try {
 721             File targetDirObj = new File(targetDir);
 722             File destDirParentObj = new File(destDir);
 723             File destDirObj = new File(destDirParentObj, targetDirObj.getName());
 724             if (! destDirParentObj.exists()) {
 725                 destDirParentObj.mkdir();
 726             }
 727             if (! destDirObj.exists()) {
 728                 destDirObj.mkdir();
 729             }
 730             String[] files = targetDirObj.list();
 731             for (String file : files) {
 732                 File srcFile = new File(targetDirObj, file);
 733                 File destFile = new File(destDirObj, file);
 734                 if (srcFile.isFile()) {
 735                     out.println("Copying " + srcFile + " to " + destFile);
 736                     copyFile(destFile, srcFile);
 737                 } else if(srcFile.isDirectory()) {
 738                     copyDir(srcFile.getAbsolutePath(), destDirObj.getAbsolutePath());
 739                 }
 740             }
 741         } catch (IOException exc) {
 742             throw new Error("Could not copy " + targetDir + " to " + destDir);
 743         }
 744     }
 745 
 746     /**
 747      * Copies a file.
 748      *
 749      * @param destfile the destination file
 750      * @param srcfile the source file
 751      * @throws IOException
 752      */
 753     public void copyFile(File destfile, File srcfile) throws IOException {
 754         Files.copy(srcfile.toPath(), destfile.toPath());
 755     }
 756 
 757     /**
 758      * Read a file from the output directory.
 759      *
 760      * @param fileName  the name of the file to read
 761      * @return          the file in string format
 762      */
 763     public String readOutputFile(String fileName) throws Error {
 764         return readFile(outputDir, fileName);
 765     }
 766 
 767     protected String readFile(String fileName) throws Error {
 768         return readFile(outputDir, fileName);
 769     }
 770 
 771     protected String readFile(String baseDir, String fileName) throws Error {
 772         return readFile(new File(baseDir), fileName);
 773     }
 774 
 775     protected String readFile(Path file) {
 776         File baseDir;
 777         if (file.startsWith(outputDir.toPath())) {
 778             baseDir = outputDir;
 779         } else if (file.startsWith(currDir)) {
 780             baseDir = currDir.toFile();
 781         } else {
 782             baseDir = file.getParent().toFile();
 783         }
 784         String fileName = baseDir.toPath().relativize(file).toString();
 785         return readFile(baseDir, fileName);
 786     }
 787 
 788     /**
 789      * Reads the file and return it as a string.
 790      *
 791      * @param baseDir   the directory in which to locate the file
 792      * @param fileName  the name of the file to read
 793      * @return          the file in string format
 794      */
 795     private String readFile(File baseDir, String fileName) throws Error {
 796         if (!Objects.equals(fileContentCacheCharset, charset)) {
 797             fileContentCache.clear();
 798             fileContentCacheCharset = charset;
 799         }
 800         try {
 801             File file = new File(baseDir, fileName);
 802             SoftReference<String> ref = fileContentCache.get(file);
 803             String content = (ref == null) ? null : ref.get();
 804             if (content != null)
 805                 return content;
 806 
 807             // charset defaults to a value inferred from latest javadoc run
 808             content = new String(Files.readAllBytes(file.toPath()), charset);
 809             fileContentCache.put(file, new SoftReference<>(content));
 810             return content;
 811         } catch (FileNotFoundException e) {
 812             throw new Error("File not found: " + fileName + ": " + e);
 813         } catch (IOException e) {
 814             throw new Error("Error reading file: " + fileName + ": " + e);
 815         }
 816     }
 817 
 818     protected void checking(String message) {
 819         numTestsRun++;
 820         javadocTestNum++;
 821         print("Starting subtest " + javadocRunNum + "." + javadocTestNum, message);
 822     }
 823 
 824     protected void passed(File file, String message) {
 825         passed(file + ": " + message);
 826     }
 827 
 828     protected void passed(String message) {
 829         numTestsPassed++;
 830         print("Passed", message);
 831         out.println();
 832     }
 833 
 834     protected void failed(File file, String message) {
 835         failed(file + ": " + message);
 836     }
 837 
 838     protected void failed(String message) {
 839         print("FAILED", message);
 840         StackWalker.getInstance().walk(s -> {
 841             s.dropWhile(f -> f.getMethodName().equals("failed"))
 842                     .takeWhile(f -> !f.getMethodName().equals("runTests"))
 843                     .forEach(f -> out.println("        at "
 844                             + f.getClassName() + "." + f.getMethodName()
 845                             + "(" + f.getFileName() + ":" + f.getLineNumber() + ")"));
 846             return null;
 847         });
 848         out.println();
 849     }
 850 
 851     private void print(String prefix, String message) {
 852         if (message.isEmpty())
 853             out.println(prefix);
 854         else {
 855             out.print(prefix);
 856             out.print(": ");
 857             out.print(message.replace("\n", NL));
 858             if (!(message.endsWith("\n") || message.endsWith(NL))) {
 859                 out.println();
 860             }
 861         }
 862     }
 863 
 864     /**
 865      * Prints a summary of the test results.
 866      */
 867     protected void printSummary() {
 868         String javadocRuns = (javadocRunNum <= 1) ? ""
 869                 : ", in " + javadocRunNum + " runs of javadoc";
 870 
 871         if (numTestsRun != 0 && numTestsPassed == numTestsRun) {
 872             // Test passed
 873             out.println();
 874             out.println("All " + numTestsPassed + " subtests passed" + javadocRuns);
 875         } else {
 876             // Test failed
 877             throw new Error((numTestsRun - numTestsPassed)
 878                     + " of " + (numTestsRun)
 879                     + " subtests failed"
 880                     + javadocRuns);
 881         }
 882     }
 883 
 884     /**
 885      * Searches for the string in the given file and return true
 886      * if the string was found.
 887      *
 888      * @param fileString    the contents of the file to search through
 889      * @param stringToFind  the string to search for
 890      * @return              true if the string was found
 891      */
 892     private boolean findString(String fileString, String stringToFind) {
 893         // javadoc (should) always use the platform newline sequence,
 894         // but in the strings to find it is more convenient to use the Java
 895         // newline character. So we translate \n to NL before we search.
 896         stringToFind = stringToFind.replace("\n", NL);
 897         return fileString.contains(stringToFind);
 898     }
 899 
 900     /**
 901      * Compares the two given files.
 902      *
 903      * @param baseDir1 the directory in which to locate the first file
 904      * @param baseDir2 the directory in which to locate the second file
 905      * @param file the file to compare in the two base directories
 906      * an error if the files do not match.
 907      * @return true if the files are the same and false otherwise.
 908      */
 909     private void diff(File baseDir1, File baseDir2, String file) {
 910         String file1Contents = readFile(baseDir1, file);
 911         String file2Contents = readFile(baseDir2, file);
 912         checking("diff " + new File(baseDir1, file) + ", " + new File(baseDir2, file));
 913         if (file1Contents.trim().compareTo(file2Contents.trim()) == 0) {
 914             passed("files are equal");
 915         } else {
 916             failed("files differ");
 917         }
 918     }
 919 
 920     /**
 921      * Utility class to simplify the handling of temporarily setting a
 922      * new stream for System.out or System.err.
 923      */
 924     private static class StreamOutput {
 925         // functional interface to set a stream.
 926         private interface Initializer {
 927             void set(PrintStream s);
 928         }
 929 
 930         private final ByteArrayOutputStream baos = new ByteArrayOutputStream();
 931         private final PrintStream ps = new PrintStream(baos);
 932         private final PrintStream prev;
 933         private final Initializer init;
 934 
 935         StreamOutput(PrintStream s, Initializer init) {
 936             prev = s;
 937             init.set(ps);
 938             this.init = init;
 939         }
 940 
 941         String close() {
 942             init.set(prev);
 943             ps.close();
 944             return baos.toString();
 945         }
 946     }
 947 
 948     /**
 949      * Utility class to simplify the handling of creating an in-memory PrintWriter.
 950      */
 951     private static class WriterOutput {
 952         private final StringWriter sw = new StringWriter();
 953         final PrintWriter pw = new PrintWriter(sw);
 954         String close() {
 955             pw.close();
 956             return sw.toString();
 957         }
 958     }
 959 
 960 
 961 //    private final Logger log = new Logger();
 962 
 963     //--------- Logging --------------------------------------------------------
 964     //
 965     // This class writes out the details of calls to checkOutput and checkFile
 966     // in a canonical way, so that the resulting file can be checked against
 967     // similar files from other versions of JavadocTester using the same logging
 968     // facilities.
 969 
 970     static class Logger {
 971         private static final int PREFIX = 40;
 972         private static final int SUFFIX = 20;
 973         private static final int MAX = PREFIX + SUFFIX;
 974         List<String> tests = new ArrayList<>();
 975         String outDir;
 976         String rootDir = rootDir();
 977 
 978         static String rootDir() {
 979             File f = new File(".").getAbsoluteFile();
 980             while (!new File(f, ".hg").exists())
 981                 f = f.getParentFile();
 982             return f.getPath();
 983         }
 984 
 985         void setOutDir(File outDir) {
 986             this.outDir = outDir.getPath();
 987         }
 988 
 989         void logCheckFile(String file, boolean positive) {
 990             // Strip the outdir because that will typically not be the same
 991             if (file.startsWith(outDir + "/"))
 992                 file = file.substring(outDir.length() + 1);
 993             tests.add(file + " " + positive);
 994         }
 995 
 996         void logCheckOutput(String file, boolean positive, String text) {
 997             // Compress the string to be displayed in the log file
 998             String simpleText = text.replaceAll("\\s+", " ").replace(rootDir, "[ROOT]");
 999             if (simpleText.length() > MAX)
1000                 simpleText = simpleText.substring(0, PREFIX)
1001                         + "..." + simpleText.substring(simpleText.length() - SUFFIX);
1002             // Strip the outdir because that will typically not be the same
1003             if (file.startsWith(outDir + "/"))
1004                 file = file.substring(outDir.length() + 1);
1005             // The use of text.hashCode ensure that all of "text" is taken into account
1006             tests.add(file + " " + positive + " " + text.hashCode() + " " + simpleText);
1007         }
1008 
1009         void write() {
1010             // sort the log entries because the subtests may not be executed in the same order
1011             tests.sort((a, b) -> a.compareTo(b));
1012             try (BufferedWriter bw = new BufferedWriter(new FileWriter("tester.log"))) {
1013                 for (String t: tests) {
1014                     bw.write(t);
1015                     bw.newLine();
1016                 }
1017             } catch (IOException e) {
1018                 throw new Error("problem writing log: " + e);
1019             }
1020         }
1021     }
1022 
1023     // Support classes for checkLinks
1024 
1025 }