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 }