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