1 /*
   2  * Copyright (c) 2002, 2015, 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 import java.io.BufferedWriter;
  25 import java.io.ByteArrayOutputStream;
  26 import java.io.File;
  27 import java.io.FileNotFoundException;
  28 import java.io.FileWriter;
  29 import java.io.FilenameFilter;
  30 import java.io.IOException;
  31 import java.io.PrintStream;
  32 import java.io.PrintWriter;
  33 import java.io.StringWriter;
  34 import java.lang.annotation.Annotation;
  35 import java.lang.annotation.Retention;
  36 import java.lang.annotation.RetentionPolicy;
  37 import java.lang.ref.SoftReference;
  38 import java.lang.reflect.InvocationTargetException;
  39 import java.lang.reflect.Method;
  40 import java.nio.file.Files;
  41 import java.util.Arrays;
  42 import java.util.ArrayList;
  43 import java.util.EnumMap;
  44 import java.util.HashMap;
  45 import java.util.List;
  46 import java.util.Map;
  47 
  48 
  49 /**
  50  * Test framework for running javadoc and performing tests on the resulting output.
  51  *
  52  * <p>
  53  * Tests are typically written as subtypes of JavadocTester, with a main
  54  * method that creates an instance of the test class and calls the runTests()
  55  * method. The runTests() methods calls all the test methods declared in the class,
  56  * and then calls a method to print a summary, and throw an exception if
  57  * any of the test methods reported a failure.
  58  *
  59  * <p>
  60  * Test methods are identified with a @Test annotation. They have no parameters.
  61  * The name of the method is not important, but if you have more than one, it is
  62  * recommended that the names be meaningful and suggestive of the test case
  63  * contained therein.
  64  *
  65  * <p>
  66  * Typically, a test method will invoke javadoc, and then perform various
  67  * checks on the results. The standard checks are:
  68  *
  69  * <dl>
  70  * <dt>checkExitCode
  71  * <dd>Check the exit code returned from javadoc.
  72  * <dt>checkOutput
  73  * <dd>Perform a series of checks on the contents on a file or output stream
  74  *     generated by javadoc.
  75  *     The checks can be either that a series of strings are found or are not found.
  76  * <dt>checkFiles
  77  * <dd>Perform a series of checks on the files generated by javadoc.
  78  *     The checks can be that a series of files are found or are not found.
  79  * </dl>
  80  *
  81  * <pre><code>
  82  *  public class MyTester extends JavadocTester {
  83  *      public static void main(String... args) throws Exception {
  84  *          MyTester tester = new MyTester();
  85  *          tester.runTests();
  86  *      }
  87  *
  88  *      // test methods...
  89  *      @Test
  90  *      void test() {
  91  *          javadoc(<i>args</i>);
  92  *          checkExit(Exit.OK);
  93  *          checkOutput(<i>file</i>, true,
  94  *              <i>strings-to-find</i>);
  95  *          checkOutput(<i>file</i>, false,
  96  *              <i>strings-to-not-find</i>);
  97  *      }
  98  *  }
  99  * </code></pre>
 100  *
 101  * <p>
 102  * If javadoc is run more than once in a test method, you can compare the
 103  * results that are generated with the diff method. Since files written by
 104  * javadoc typically contain a timestamp, you may want to use the -notimestamp
 105  * option if you are going to compare the results from two runs of javadoc.
 106  *
 107  * <p>
 108  * If you have many calls of checkOutput that are very similar, you can write
 109  * your own check... method to reduce the amount of duplication. For example,
 110  * if you want to check that many files contain the same string, you could
 111  * write a method that takes a varargs list of files and calls checkOutput
 112  * on each file in turn with the string to be checked.
 113  *
 114  * <p>
 115  * You can also write you own custom check methods, which can use
 116  * readFile to get the contents of a file generated by javadoc,
 117  * and then use pass(...) or fail(...) to report whether the check
 118  * succeeded or not.
 119  *
 120  * <p>
 121  * You can have many separate test methods, each identified with a @Test
 122  * annotation. However, you should <b>not</b> assume they will be called
 123  * in the order declared in your source file.  If the order of a series
 124  * of javadoc invocations is important, do that within a single method.
 125  * If the invocations are independent, for better clarity, use separate
 126  * test methods, each with their own set of checks on the results.
 127  *
 128  * @author Doug Kramer
 129  * @author Jamie Ho
 130  * @author Jonathan Gibbons (rewrite)
 131  */
 132 public abstract class JavadocTester {
 133 
 134     public static final String FS = System.getProperty("file.separator");
 135     public static final String PS = System.getProperty("path.separator");
 136     public static final String NL = System.getProperty("line.separator");
 137 
 138     public enum Output {
 139         /** The name of the output stream from javadoc. */
 140         OUT,
 141         /** The name for any output written to System.out. */
 142         STDOUT,
 143         /** The name for any output written to System.err. */
 144         STDERR
 145     }
 146 
 147     /** The output directory used in the most recent call of javadoc. */
 148     protected File outputDir;
 149 
 150     /** The exit code of the most recent call of javadoc. */
 151     private int exitCode;
 152 
 153     /** The output generated by javadoc to the various writers and streams. */
 154     private final Map<Output, String> outputMap = new EnumMap<>(Output.class);
 155 
 156     /** A cache of file content, to avoid reading files unnecessarily. */
 157     private final Map<File,SoftReference<String>> fileContentCache = new HashMap<>();
 158 
 159     /** Stream used for logging messages. */
 160     private final PrintStream out = System.out;
 161 
 162     /** The directory containing the source code for the test. */
 163     public static final String testSrc = System.getProperty("test.src");
 164 
 165     /**
 166      * Get the path for a source file in the test source directory.
 167      * @param path the path of a file or directory in the source directory
 168      * @return the full path of the specified file
 169      */
 170     public static String testSrc(String path) {
 171         return new File(testSrc, path).getPath();
 172     }
 173 
 174     /**
 175      * Alternatives for checking the contents of a directory.
 176      */
 177     public enum DirectoryCheck {
 178         /**
 179          * Check that the directory is empty.
 180          */
 181         EMPTY((file, name) -> true),
 182         /**
 183          * Check that the directory does not contain any HTML files,
 184          * such as may have been generated by a prior run of javadoc
 185          * using this directory.
 186          * For now, the check is only performed on the top level directory.
 187          */
 188         NO_HTML_FILES((file, name) -> name.endsWith(".html")),
 189         /**
 190          * No check is performed on the directory contents.
 191          */
 192         NONE(null) { @Override void check(File dir) { } };
 193 
 194         /** The filter used to detect that files should <i>not</i> be present. */
 195         FilenameFilter filter;
 196 
 197         DirectoryCheck(FilenameFilter f) {
 198             filter = f;
 199         }
 200 
 201         void check(File dir) {
 202             if (dir.isDirectory()) {
 203                 String[] contents = dir.list(filter);
 204                 if (contents == null)
 205                     throw new Error("cannot list directory: " + dir);
 206                 if (contents.length > 0) {
 207                     System.err.println("Found extraneous files in dir:" + dir.getAbsolutePath());
 208                     for (String x : contents) {
 209                         System.err.println(x);
 210                     }
 211                     throw new Error("directory has unexpected content: " + dir);
 212                 }
 213             }
 214         }
 215     }
 216 
 217     private DirectoryCheck outputDirectoryCheck = DirectoryCheck.EMPTY;
 218 
 219     /** The current subtest number. Incremented when checking(...) is called. */
 220     private int numTestsRun = 0;
 221 
 222     /** The number of subtests passed. Incremented when passed(...) is called. */
 223     private int numTestsPassed = 0;
 224 
 225     /** The current run of javadoc. Incremented when javadoc is called. */
 226     private int javadocRunNum = 0;
 227 
 228     /** Marker annotation for test methods to be invoked by runTests. */
 229     @Retention(RetentionPolicy.RUNTIME)
 230     @interface Test { }
 231 
 232     /**
 233      * Run all methods annotated with @Test, followed by printSummary.
 234      * Typically called on a tester object in main()
 235      * @throws Exception if any errors occurred
 236      */
 237     public void runTests() throws Exception {
 238         for (Method m: getClass().getDeclaredMethods()) {
 239             Annotation a = m.getAnnotation(Test.class);
 240             if (a != null) {
 241                 try {
 242                     out.println("Running test " + m.getName());
 243                     m.invoke(this, new Object[] { });
 244                 } catch (InvocationTargetException e) {
 245                     Throwable cause = e.getCause();
 246                     throw (cause instanceof Exception) ? ((Exception) cause) : e;
 247                 }
 248                 out.println();
 249             }
 250         }
 251         printSummary();
 252     }
 253 
 254     /**
 255      * Run javadoc.
 256      * The output directory used by this call and the final exit code
 257      * will be saved for later use.
 258      * To aid the reader, it is recommended that calls to this method
 259      * put each option and the arguments it takes on a separate line.
 260      *
 261      * Example:
 262      * <pre><code>
 263      *  javadoc("-d", "out",
 264      *          "-sourcepath", testSrc,
 265      *          "-notimestamp",
 266      *          "pkg1", "pkg2", "pkg3/C.java");
 267      * </code></pre>
 268      *
 269      * @param args the arguments to pass to javadoc
 270      */
 271     public void javadoc(String... args) {
 272         outputMap.clear();
 273         fileContentCache.clear();
 274 
 275         javadocRunNum++;
 276         if (javadocRunNum == 1) {
 277             out.println("Running javadoc...");
 278         } else {
 279             out.println("Running javadoc (run "
 280                                     + javadocRunNum + ")...");
 281         }
 282         outputDir = new File(".");
 283         for (int i = 0; i < args.length - 2; i++) {
 284             if (args[i].equals("-d")) {
 285                 outputDir = new File(args[++i]);
 286                 break;
 287             }
 288         }
 289         out.println("args: " + Arrays.toString(args));
 290 //        log.setOutDir(outputDir);
 291 
 292         outputDirectoryCheck.check(outputDir);
 293 
 294         // This is the sole stream used by javadoc
 295         WriterOutput outOut = new WriterOutput();
 296 
 297         // These are to catch output to System.out and System.err,
 298         // in case these are used instead of the primary streams
 299         StreamOutput sysOut = new StreamOutput(System.out, System::setOut);
 300         StreamOutput sysErr = new StreamOutput(System.err, System::setErr);
 301 
 302         try {
 303             exitCode = jdk.javadoc.internal.tool.Main.execute(args, outOut.pw);
 304         } finally {
 305             outputMap.put(Output.STDOUT, sysOut.close());
 306             outputMap.put(Output.STDERR, sysErr.close());
 307             outputMap.put(Output.OUT, outOut.close());
 308         }
 309 
 310         outputMap.forEach((name, text) -> {
 311             if (!text.isEmpty()) {
 312                 out.println("javadoc " + name + ":");
 313                 out.println(text);
 314             }
 315         });
 316     }
 317 
 318     /**
 319      * Set the kind of check for the initial contents of the output directory
 320      * before javadoc is run.
 321      * The filter should return true for files that should <b>not</b> appear.
 322      * @param c the kind of check to perform
 323      */
 324     public void setOutputDirectoryCheck(DirectoryCheck c) {
 325         outputDirectoryCheck = c;
 326     }
 327 
 328     public enum Exit {
 329         OK(0),
 330         FAILED(1);
 331 
 332         Exit(int code) {
 333             this.code = code;
 334         }
 335 
 336         final int code;
 337     }
 338 
 339     /**
 340      * Check the exit code of the most recent call of javadoc.
 341      *
 342      * @param expected the exit code that is required for the test
 343      * to pass.
 344      */
 345     public void checkExit(Exit expected) {
 346         checking("check exit code");
 347         if (exitCode == expected.code) {
 348             passed("return code " + exitCode);
 349         } else {
 350             failed("return code " + exitCode +"; expected " + expected.code + " (" + expected + ")");
 351         }
 352     }
 353 
 354     /**
 355      * Check for content in (or not in) the generated output.
 356      * Within the search strings, the newline character \n
 357      * will be translated to the platform newline character sequence.
 358      * @param path a path within the most recent output directory
 359      *  or the name of one of the output buffers, identifying
 360      *  where to look for the search strings.
 361      * @param expectedFound true if all of the search strings are expected
 362      *  to be found, or false if all of the strings are expected to be
 363      *  not found
 364      * @param strings the strings to be searched for
 365      */
 366     public void checkOutput(String path, boolean expectedFound, String... strings) {
 367         // Read contents of file
 368         String fileString;
 369         try {
 370             fileString = readFile(outputDir, path);
 371         } catch (Error e) {
 372             if (!expectedFound) {
 373                 failed("Error reading file: " + e);
 374                 return;
 375             }
 376             throw e;
 377         }
 378         checkOutput(path, fileString, expectedFound, strings);
 379     }
 380 
 381     /**
 382      * Check for content in (or not in) the one of the output streams written by
 383      * javadoc. Within the search strings, the newline character \n
 384      * will be translated to the platform newline character sequence.
 385      * @param output the output stream to check
 386      * @param expectedFound true if all of the search strings are expected
 387      *  to be found, or false if all of the strings are expected to be
 388      *  not found
 389      * @param strings the strings to be searched for
 390      */
 391     public void checkOutput(Output output, boolean expectedFound, String... strings) {
 392         checkOutput(output.toString(), outputMap.get(output), expectedFound, strings);
 393     }
 394 
 395     private void checkOutput(String path, String fileString, boolean expectedFound, String... strings) {
 396         for (String stringToFind : strings) {
 397 //            log.logCheckOutput(path, expectedFound, stringToFind);
 398             checking("checkOutput");
 399             // Find string in file's contents
 400             boolean isFound = findString(fileString, stringToFind);
 401             if (isFound == expectedFound) {
 402                 passed(path + ": " + (isFound ? "found:" : "not found:") + "\n"
 403                         + stringToFind + "\n");
 404             } else {
 405                 failed(path + ": " + (isFound ? "found:" : "not found:") + "\n"
 406                         + stringToFind + "\n");
 407             }
 408         }
 409     }
 410 
 411     /**
 412      * Check for files in (or not in) the generated output.
 413      * @param expectedFound true if all of the files are expected
 414      *  to be found, or false if all of the files are expected to be
 415      *  not found
 416      * @param paths the files to check, within the most recent output directory.
 417      * */
 418     public void checkFiles(boolean expectedFound, String... paths) {
 419         for (String path: paths) {
 420 //            log.logCheckFile(path, expectedFound);
 421             checking("checkFile");
 422             File file = new File(outputDir, path);
 423             boolean isFound = file.exists();
 424             if (isFound == expectedFound) {
 425                 passed(path + ": " + (isFound ? "found:" : "not found:") + "\n");
 426             } else {
 427                 failed(path + ": " + (isFound ? "found:" : "not found:") + "\n");
 428             }
 429         }
 430     }
 431 
 432     /**
 433      * Check that a series of strings are found in order in a file in
 434      * the generated output.
 435      * @param path the file to check
 436      * @param strings  the strings whose order to check
 437      */
 438     public void checkOrder(String path, String... strings) {
 439         String fileString = readOutputFile(path);
 440         int prevIndex = -1;
 441         for (String s : strings) {
 442             s = s.replace("\n", NL); // normalize new lines
 443             int currentIndex = fileString.indexOf(s);
 444             checking(s + " at index " + currentIndex);
 445             if (currentIndex == -1) {
 446                 failed(s + " not found.");
 447                 continue;
 448             }
 449             if (currentIndex > prevIndex) {
 450                 passed(s + " is in the correct order");
 451             } else {
 452                 failed("file: " + path + ": " + s + " is in the wrong order.");
 453             }
 454             prevIndex = currentIndex;
 455         }
 456     }
 457 
 458     /**
 459      * Ensures that a series of strings appear only once, in the generated output,
 460      * noting that, this test does not exhaustively check for all other possible
 461      * duplicates once one is found.
 462      * @param path the file to check
 463      * @param strings ensure each are unique
 464      */
 465     public void checkUnique(String path, String... strings) {
 466         String fileString = readOutputFile(path);
 467         for (String s : strings) {
 468             int currentIndex = fileString.indexOf(s);
 469             checking(s + " at index " + currentIndex);
 470             if (currentIndex == -1) {
 471                 failed(s + " not found.");
 472                 continue;
 473             }
 474             int nextindex = fileString.indexOf(s, currentIndex + s.length());
 475             if (nextindex == -1) {
 476                 passed(s + " is unique");
 477             } else {
 478                 failed(s + " is not unique, found at " + nextindex);
 479             }
 480         }
 481     }
 482 
 483     /**
 484      * Compare a set of files in each of two directories.
 485      *
 486      * @param baseDir1 the directory containing the first set of files
 487      * @param baseDir2 the directory containing the second set of files
 488      * @param files the set of files to be compared
 489      */
 490     public void diff(String baseDir1, String baseDir2, String... files) {
 491         File bd1 = new File(baseDir1);
 492         File bd2 = new File(baseDir2);
 493         for (String file : files) {
 494             diff(bd1, bd2, file);
 495         }
 496     }
 497 
 498     /**
 499      * A utility to copy a directory from one place to another.
 500      *
 501      * @param targetDir the directory to copy.
 502      * @param destDir the destination to copy the directory to.
 503      */
 504     // TODO: convert to using java.nio.Files.walkFileTree
 505     public void copyDir(String targetDir, String destDir) {
 506         try {
 507             File targetDirObj = new File(targetDir);
 508             File destDirParentObj = new File(destDir);
 509             File destDirObj = new File(destDirParentObj, targetDirObj.getName());
 510             if (! destDirParentObj.exists()) {
 511                 destDirParentObj.mkdir();
 512             }
 513             if (! destDirObj.exists()) {
 514                 destDirObj.mkdir();
 515             }
 516             String[] files = targetDirObj.list();
 517             for (String file : files) {
 518                 File srcFile = new File(targetDirObj, file);
 519                 File destFile = new File(destDirObj, file);
 520                 if (srcFile.isFile()) {
 521                     out.println("Copying " + srcFile + " to " + destFile);
 522                     copyFile(destFile, srcFile);
 523                 } else if(srcFile.isDirectory()) {
 524                     copyDir(srcFile.getAbsolutePath(), destDirObj.getAbsolutePath());
 525                 }
 526             }
 527         } catch (IOException exc) {
 528             throw new Error("Could not copy " + targetDir + " to " + destDir);
 529         }
 530     }
 531 
 532     /**
 533      * Copy source file to destination file.
 534      *
 535      * @param destfile the destination file
 536      * @param srcfile the source file
 537      * @throws IOException
 538      */
 539     public void copyFile(File destfile, File srcfile) throws IOException {
 540         Files.copy(srcfile.toPath(), destfile.toPath());
 541     }
 542 
 543     /**
 544      * Read a file from the output directory.
 545      *
 546      * @param fileName  the name of the file to read
 547      * @return          the file in string format
 548      */
 549     public String readOutputFile(String fileName) throws Error {
 550         return readFile(outputDir, fileName);
 551     }
 552 
 553     protected String readFile(String fileName) throws Error {
 554         return readFile(outputDir, fileName);
 555     }
 556 
 557     protected String readFile(String baseDir, String fileName) throws Error {
 558         return readFile(new File(baseDir), fileName);
 559     }
 560 
 561     /**
 562      * Read the file and return it as a string.
 563      *
 564      * @param baseDir   the directory in which to locate the file
 565      * @param fileName  the name of the file to read
 566      * @return          the file in string format
 567      */
 568     private String readFile(File baseDir, String fileName) throws Error {
 569         try {
 570             File file = new File(baseDir, fileName);
 571             SoftReference<String> ref = fileContentCache.get(file);
 572             String content = (ref == null) ? null : ref.get();
 573             if (content != null)
 574                 return content;
 575 
 576             content = new String(Files.readAllBytes(file.toPath()));
 577             fileContentCache.put(file, new SoftReference(content));
 578             return content;
 579         } catch (FileNotFoundException e) {
 580             System.err.println(e);
 581             throw new Error("File not found: " + fileName);
 582         } catch (IOException e) {
 583             System.err.println(e);
 584             throw new Error("Error reading file: " + fileName);
 585         }
 586     }
 587 
 588     protected void checking(String message) {
 589         numTestsRun++;
 590         print("Starting subtest " + numTestsRun, message);
 591     }
 592 
 593     protected void passed(String message) {
 594         numTestsPassed++;
 595         print("Passed", message);
 596     }
 597 
 598     protected void failed(String message) {
 599         print("FAILED", message);
 600     }
 601 
 602     private void print(String prefix, String message) {
 603         if (message.isEmpty())
 604             out.println(prefix);
 605         else {
 606             out.print(prefix);
 607             out.print(": ");
 608             out.println(message.replace("\n", NL));
 609         }
 610     }
 611 
 612     /**
 613      * Print a summary of the test results.
 614      */
 615     protected void printSummary() {
 616 //        log.write();
 617         if (numTestsRun != 0 && numTestsPassed == numTestsRun) {
 618             // Test passed
 619             out.println();
 620             out.println("All " + numTestsPassed + " subtests passed");
 621         } else {
 622             // Test failed
 623             throw new Error((numTestsRun - numTestsPassed)
 624                     + " of " + (numTestsRun)
 625                     + " subtests failed");
 626         }
 627     }
 628 
 629     /**
 630      * Search for the string in the given file and return true
 631      * if the string was found.
 632      *
 633      * @param fileString    the contents of the file to search through
 634      * @param stringToFind  the string to search for
 635      * @return              true if the string was found
 636      */
 637     private boolean findString(String fileString, String stringToFind) {
 638         // javadoc (should) always use the platform newline sequence,
 639         // but in the strings to find it is more convenient to use the Java
 640         // newline character. So we translate \n to NL before we search.
 641         stringToFind = stringToFind.replace("\n", NL);
 642         return fileString.contains(stringToFind);
 643     }
 644 
 645     /**
 646      * Compare the two given files.
 647      *
 648      * @param baseDir1 the directory in which to locate the first file
 649      * @param baseDir2 the directory in which to locate the second file
 650      * @param file the file to compare in the two base directories
 651      * @param throwErrorIFNoMatch flag to indicate whether or not to throw
 652      * an error if the files do not match.
 653      * @return true if the files are the same and false otherwise.
 654      */
 655     private void diff(File baseDir1, File baseDir2, String file) {
 656         String file1Contents = readFile(baseDir1, file);
 657         String file2Contents = readFile(baseDir2, file);
 658         checking("diff " + new File(baseDir1, file) + ", " + new File(baseDir2, file));
 659         if (file1Contents.trim().compareTo(file2Contents.trim()) == 0) {
 660             passed("files are equal");
 661         } else {
 662             failed("files differ");
 663         }
 664     }
 665 
 666     /**
 667      * Utility class to simplify the handling of temporarily setting a
 668      * new stream for System.out or System.err.
 669      */
 670     private static class StreamOutput {
 671         // functional interface to set a stream.
 672         private interface Initializer {
 673             void set(PrintStream s);
 674         }
 675 
 676         private final ByteArrayOutputStream baos = new ByteArrayOutputStream();
 677         private final PrintStream ps = new PrintStream(baos);
 678         private final PrintStream prev;
 679         private final Initializer init;
 680 
 681         StreamOutput(PrintStream s, Initializer init) {
 682             prev = s;
 683             init.set(ps);
 684             this.init = init;
 685         }
 686 
 687         String close() {
 688             init.set(prev);
 689             ps.close();
 690             return baos.toString();
 691         }
 692     }
 693 
 694     /**
 695      * Utility class to simplify the handling of creating an in-memory PrintWriter.
 696      */
 697     private static class WriterOutput {
 698         private final StringWriter sw = new StringWriter();
 699         final PrintWriter pw = new PrintWriter(sw);
 700         String close() {
 701             pw.close();
 702             return sw.toString();
 703         }
 704     }
 705 
 706 
 707 //    private final Logger log = new Logger();
 708 
 709     //--------- Logging --------------------------------------------------------
 710     //
 711     // This class writes out the details of calls to checkOutput and checkFile
 712     // in a canonical way, so that the resulting file can be checked against
 713     // similar files from other versions of JavadocTester using the same logging
 714     // facilities.
 715 
 716     static class Logger {
 717         private static final int PREFIX = 40;
 718         private static final int SUFFIX = 20;
 719         private static final int MAX = PREFIX + SUFFIX;
 720         List<String> tests = new ArrayList<>();
 721         String outDir;
 722         String rootDir = rootDir();
 723 
 724         static String rootDir() {
 725             File f = new File(".").getAbsoluteFile();
 726             while (!new File(f, ".hg").exists())
 727                 f = f.getParentFile();
 728             return f.getPath();
 729         }
 730 
 731         void setOutDir(File outDir) {
 732             this.outDir = outDir.getPath();
 733         }
 734 
 735         void logCheckFile(String file, boolean positive) {
 736             // Strip the outdir because that will typically not be the same
 737             if (file.startsWith(outDir + "/"))
 738                 file = file.substring(outDir.length() + 1);
 739             tests.add(file + " " + positive);
 740         }
 741 
 742         void logCheckOutput(String file, boolean positive, String text) {
 743             // Compress the string to be displayed in the log file
 744             String simpleText = text.replaceAll("\\s+", " ").replace(rootDir, "[ROOT]");
 745             if (simpleText.length() > MAX)
 746                 simpleText = simpleText.substring(0, PREFIX)
 747                         + "..." + simpleText.substring(simpleText.length() - SUFFIX);
 748             // Strip the outdir because that will typically not be the same
 749             if (file.startsWith(outDir + "/"))
 750                 file = file.substring(outDir.length() + 1);
 751             // The use of text.hashCode ensure that all of "text" is taken into account
 752             tests.add(file + " " + positive + " " + text.hashCode() + " " + simpleText);
 753         }
 754 
 755         void write() {
 756             // sort the log entries because the subtests may not be executed in the same order
 757             tests.sort((a, b) -> a.compareTo(b));
 758             try (BufferedWriter bw = new BufferedWriter(new FileWriter("tester.log"))) {
 759                 for (String t: tests) {
 760                     bw.write(t);
 761                     bw.newLine();
 762                 }
 763             } catch (IOException e) {
 764                 throw new Error("problem writing log: " + e);
 765             }
 766         }
 767     }
 768 }