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 }