/*
 * $Id$
 *
 * Copyright (c) 1996, 2016, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */
package com.sun.javatest;

import com.sun.javatest.util.BackupPolicy;
import com.sun.javatest.util.DynamicArray;
import com.sun.javatest.util.I18NResourceBundle;
import com.sun.javatest.util.PropertyArray;
import com.sun.javatest.util.PropertyUtils;
import com.sun.javatest.util.StringArray;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.Reader;
import java.io.StringReader;
import java.io.Writer;
import java.lang.ref.WeakReference;
import java.nio.charset.StandardCharsets;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Vector;

/**
 * The TestResult object encapsulates the results from a test.
 * Test results are formatted in sections of command output,
 * comments and sometimes "streams" of output (stdout for example).
 * Each of these sections is represented by a (@link TestResult.Section Section).
 * Instances of this class are mutable until the result of the section is
 * set or until the result of the test itself is set.
 * <p>
 * Test results are stored in a structured text files.
 * The TestResult class serves as the API for accessing the various
 * components that make up the result of running a test.
 * The status is cached as its size is small and it is accessed often.
 * <p>
 * This class and inner classes will throw IllegalStateExceptions if an
 * attempt is made to modify the any part of the object that has been
 * marked immutable.
 */

public class TestResult {
    /**
     * A code indicating that no checksum was found in a .jtr file.
     *
     * @see #getChecksumState
     */
    public static final int NO_CHECKSUM = 0;
    /**
     * A code indicating that an invalid checksum was found in a .jtr file.
     *
     * @see #getChecksumState
     */
    public static final int BAD_CHECKSUM = 1;
    /**
     * A code indicating that a good checksum was found in a .jtr file.
     *
     * @see #getChecksumState
     */
    public static final int GOOD_CHECKSUM = 2;
    /**
     * The number of different checksum states (none, good, bad).
     */
    public static final int NUM_CHECKSUM_STATES = 3;
    /**
     * The name of the default output that all Sections are equipped with.
     */
    public static final String MESSAGE_OUTPUT_NAME = "messages";
    /**
     * The name of the default section that all TestResult objects are equipped with.
     */
    public static final String MSG_SECTION_NAME = "script_messages";
    /**
     * The name of the property that defines the test description file.
     */
    public static final String DESCRIPTION = "description";


    // ------------------------- PUBLIC CONSTRUCTORS -------------------------
    /**
     * The name of the property that defines the time at which the test
     * execution finished.
     */
    public static final String END = "end";
    /**
     * The name of the property that defines the environment name.
     */
    public static final String ENVIRONMENT = "environment";
    /**
     * The name of the property that defines the test execution status.
     */
    public static final String EXEC_STATUS = "execStatus";
    /**
     * The name of the property that defines the OS on which JT Harness
     * was running when the test was run.
     */
    public static final String JAVATEST_OS = "javatestOS";
    /**
     * The name of the property that defines the script that ran the test.
     */
    public static final String SCRIPT = "script";
    /**
     * The name of the property that defines the test output sections
     * that were recorded when the test ran.
     */
    public static final String SECTIONS = "sections";

    //------------------------ MODIFIER METHODS ------------------------------
    /**
     * The name of the property that defines the time at which the test
     * execution started.
     */
    public static final String START = "start";
    /**
     * The name of the property that defines the test for which this is
     * the result object.
     */
    public static final String TEST = "test";
    /**
     * The name of the property that defines which version of JT Harness
     * was used to run the test.
     */
    public static final String VERSION = "javatestVersion";
    /**
     * The name of the property that defines the work directory for the test.
     */
    public static final String WORK = "work";
    /**
     * The name of the property that defines the variety of harness in use.
     * Generally the full harness or the lite version.
     */
    public static final String VARIETY = "harnessVariety";
    /**
     * The name of the property that defines the type of class loader used when
     * running the harness (classpath mode or module mode generally).
     */
    public static final String LOADER = "harnessLoaderMode";

    //----------ACCESS FUNCTIONS (MISC)-----------------------------------------
    /**
     * DateFormat, that is used to store date into TestResult
     */
    public static final DateFormat dateFormat =
            new SimpleDateFormat("EEE MMM dd HH:mm:ss zzz yyyy", Locale.US);
    static final String EXTN = ".jtr";
    private static final Status
            filesSame = Status.passed("Output file and reference file matched"),
            filesDifferent = Status.failed("Output file and reference file were different"),
            fileError = Status.failed("Error occurred during comparison"),
            interrupted = Status.failed("interrupted"),
            inProgress = Status.notRun("Test running..."),
            incomplete = Status.notRun("Section not closed, may be incomplete"),
            tdMismatch = Status.notRun("Old test flushed, new test description located"),
            notRunStatus = Status.notRun("");
    private static final String[] emptyStringArray = new String[0];
    private static final Section[] emptySectionArray = new Section[0];
    private static final String defaultClassDir = "classes";
    // info for reading/writing JTR files (version 1)
    private static final String JTR_V1_HEADER = "#Test Results";
    private static final String JTR_V1_SECTRESULT = "command result:";
    private static final String JTR_V1_TSTRESULT = "test result:";
    // info for reading/writing JTR files (version 2)
    private static final String JTR_V2_HEADER = "#Test Results (version 2)";

    /*
     * Get the title of this test. This info originally comes from the test
     * description, but is saved in the .jtr file as well.
     *
     * @deprecated Please query the test description for info.
    public String getTitle() {
        // hmm slight copout; would like to make sure never null in the first place
        String title = desc.getParameter("title");
        if (title == null)
            title = td.getRootRelativeURL();
    }
     */
    private static final String JTR_V2_SECTION = "#section:";
    private static final String JTR_V2_CHECKSUM = "#checksum:";
    private static final String JTR_V2_TESTDESC = "#-----testdescription-----";
    private static final String JTR_V2_RESPROPS = "#-----testresult-----";
    private static final String JTR_V2_ENVIRONMENT = "#-----environment-----";
    private static final String JTR_V2_SECTRESULT = "result: ";
    private static final String JTR_V2_TSTRESULT = "test result: ";
    private static final String JTR_V2_SECTSTREAM = "----------";
    private static final String lineSeparator = System.getProperty("line.separator");
    private static final int DEFAULT_MAX_SHRINK_LIST_SIZE = 128;
    private static final int maxShrinkListSize =
            Integer.getInteger("javatest.numCachedResults", DEFAULT_MAX_SHRINK_LIST_SIZE).intValue();

    //----------ACCESS FUNCTIONS (TEST STATUS)----------------------------------
    private static final int DEFAULT_MAX_OUTPUT_SIZE = 100000;
    private static final int commonOutputSize =
            Integer.getInteger("javatest.maxOutputSize", DEFAULT_MAX_OUTPUT_SIZE).intValue();

    //----------ACCESS METHODS (TEST OUTPUT)----------------------------------
    // because so few test results will typically be observed (e.g. at most one)
    // we don't use a per-instance array of observers, but instead associate any
    // such arrays here.
    private static Map<TestResult, Observer[]> observersTable = new Hashtable<>(16);
    private static LinkedList<WeakReference<TestResult>> shrinkList = new LinkedList<>();
    private static I18NResourceBundle i18n = I18NResourceBundle.getBundleForClass(TestResult.class);
    private static boolean debug = Boolean.getBoolean("debug." + TestResult.class.getName());
    // the following fields should be valid for all test results
    private File resultsFile;           // if set, location where test results are stored
    private Status execStatus;          // pre-compare result
    private String testURL;             // URL for this test, equal to the one in TD.getRootRelativeURL

    // -----observer methods ---------------------------------------------------
    private long endTime = -1;          // when test finished
    private byte checksumState;         // checksum state
    // the following fields are candidates for shrinking although not currently done
    private TestDescription desc;       // test description for which this is the result
    private String[] props;             // table of values written during test execution
    private String[] env;

    // ----- PACKAGE METHODS ---------------------------------------------------

    /**
     * Read a single minimal TestResult from a .jts stream.
     * The stream is not closed.
     * @deprecated JTS files are no longer supported
    TestResult(WorkDirectory workDir, DataInputStream in) throws IOException {
    workRelativePath = in.readUTF();

    // ** temp. fix ** XXX
    // make sure the path is in URL form with forward slashes
    // in the future all paths should already be of this form (TestDescription)
    int index = workRelativePath.indexOf('/');
    if (index == -1) workRelativePath = workRelativePath.replace('\\', '/');

    resultsFile = workDir.getFile(workRelativePath.replace('/', File.separatorChar));
    title = in.readUTF();
    int esc = in.readByte();
    String esr = in.readUTF();
    execStatus = new Status(esc, esr);
    boolean defIsExec = in.readBoolean();
    if (!defIsExec) {
    // have to read these, per protocol
    int dsc = in.readByte();
    String dsr = in.readUTF();
    //ignore dsc, dsr; they used to go in defStatus
    }
    }
     */
    // this field is cleared when the test result is shrunk
    @java.lang.Deprecated
    private Section[] sections;         // sections of output written during test execution
    private int maxTROutputSize = 0;    // maximum output size for this test result
    // only valid when this TR is in a TRT, should remain when shrunk
    private TestResultTable.TreeNode parent;

    /*
     * @deprecated JTS files no longer supported
    void writeSummary(DataOutputStream out) throws IOException {
        out.writeUTF(workRelativePath);
        out.writeUTF(title);
        out.writeByte(execStatus.getType());
        out.writeUTF(execStatus.getReason());
        out.writeBoolean(true); // defStatus == execStatus
    }
    */

    /**
     * Construct a test result object that will be built as the test runs.
     * The status string will be "running..." rather than "not run".
     *
     * @param td The test description to base this new object on.  Cannot be
     *           null.
     */
    public TestResult(TestDescription td) {
        desc = td;
        execStatus = inProgress;
        testURL = desc.getRootRelativeURL();

        createSection(MSG_SECTION_NAME);

        props = emptyStringArray;  // null implies it was discarded, not empty
    }

    /**
     * Reconstruct the results of a previously run test.
     *
     * @param workDir Work directory in which the tests were run
     * @param td      Description of the test that was run
     * @throws TestResult.Fault if there is a problem recreating the results
     *                          from the appropriate file in the work directory
     */
    public TestResult(TestDescription td, WorkDirectory workDir)
            throws Fault {
        desc = td;
        testURL = desc.getRootRelativeURL();
        execStatus = inProgress;

        reloadFromWorkDir(workDir);
    }

    // ----- PRIVATE METHODS ---------------------------------------------------

    /**
     * Reconstruct the results of a previously run test.
     *
     * @param file File that the results have been stored into.
     * @throws TestResult.ReloadFault             if there is a problem recreating the results
     *                                            from the given file
     * @throws TestResult.ResultFileNotFoundFault if there is a problem locating
     *                                            the given file
     */
    public TestResult(File file)
            throws ResultFileNotFoundFault, ReloadFault {
        resultsFile = file;
        reload();

        testURL = desc.getRootRelativeURL();

        execStatus = Status.parse(PropertyArray.get(props, EXEC_STATUS));
    }

    /**
     * Reconstruct the results of a previously run test.
     *
     * @param workDir          The work directory where previous results for the guven
     *                         test can be found.
     * @param workRelativePath The path to the JTR to reload, relative to the
     *                         workdir.
     * @throws TestResult.Fault if there is a problem recreating the results
     *                          from the given file
     */
    public TestResult(WorkDirectory workDir, String workRelativePath) throws Fault {
        //resultsFile = workDir.getFile(workRelativePath.replace('/', File.separatorChar));
        resultsFile = workDir.getFile(workRelativePath);
        reload();

        testURL = desc.getRootRelativeURL();
        execStatus = Status.parse(PropertyArray.get(props, EXEC_STATUS));
    }

    /**
     * Create a temporary test result for which can be handed around
     * in situations where a reasonable test result can't be created.
     *
     * @param td Description of the test
     * @param s  Status to associate with running the test... presumed
     *           to be of the Status.FAILED type.
     */
    public TestResult(TestDescription td, Status s) {
        desc = td;
        testURL = desc.getRootRelativeURL();
        resultsFile = null;
        execStatus = s;
        props = emptyStringArray;
    }

    /**
     * Read a single minimal TestResult which is capable of reloading itself.
     * None of the parameters may be null.
     *
     * @param url     The full URL of this test, including test id.
     * @param workDir The work directory location, platform specfic path.
     * @param status  The status that will be found in the JTR.
     * @throws JavaTestError Will be thrown if any params are null.
     */
    TestResult(String url, WorkDirectory workDir, Status status) {
        if (url == null) {
            throw new JavaTestError(i18n, "rslt.badTestUrl");
        }

        if (workDir == null) {
            throw new JavaTestError(i18n, "rslt.badWorkdir");
        }

        if (status == null) {
            throw new JavaTestError(i18n, "rslt.badStatus");
        }

        testURL = url;
        resultsFile = workDir.getFile(getWorkRelativePath());
        execStatus = status;
    }

    /**
     * Read a single minimal TestResult which is capable of reloading itself.
     * None of the parameters may be null.
     *
     * @param url     The full URL of this test, including test id.
     * @param workDir The work directory location, platform specific path.
     * @param status  The status that will be found in the JTR.
     * @param endTime The time when that test finished execution.
     * @throws JavaTestError Will be thrown if any params are null.
     * @see #getEndTime()
     */
    TestResult(String url, WorkDirectory workDir, Status status, long endTime) {
        if (url == null) {
            throw new JavaTestError(i18n, "rslt.badTestUrl");
        }

        if (workDir == null) {
            throw new JavaTestError(i18n, "rslt.badWorkdir");
        }

        if (status == null) {
            throw new JavaTestError(i18n, "rslt.badStatus");
        }

        testURL = url;
        resultsFile = workDir.getFile(getWorkRelativePath());
        execStatus = status;
        this.endTime = endTime;
    }

    /**
     * Create a placeholder TestResult for a test that has not yet been run.
     *
     * @param td The test description for the test
     * @return A test result that indicates that the test has not yet been run
     */
    public static TestResult notRun(TestDescription td) {
        return new TestResult(td, notRunStatus);
    }

    /**
     * Get the path name for the results file for a test, relative to the
     * work directory.  The internal separator is '/'.
     *
     * @param td the test description for the test in question
     * @return the path name for the results file for a test, relative to the
     * work directory
     */
    public static String getWorkRelativePath(TestDescription td) {
        String baseURL = td.getRootRelativePath();

        // add in uniquifying id if
        String id = td.getParameter("id");
        return getWorkRelativePath(baseURL, id);
    }

    /**
     * Get the path name for the results file for a test, relative to the
     * work directory.  The internal separator is '/'.
     *
     * @param testURL May not be null;
     * @return The work relative path of the JTR for this test.  Null if the
     * given URL is null.
     */
    public static String getWorkRelativePath(String testURL) {
        int pound = testURL.lastIndexOf('#');
        if (pound == -1)        // no test id
        {
            return getWorkRelativePath(testURL, null);
        } else {
            return getWorkRelativePath(testURL.substring(0, pound),
                    testURL.substring(pound + 1));
        }
    }

    /**
     * Get the path name for the results file for a test, relative to the
     * work directory.  The internal separator is '/'.
     *
     * @param baseURL May not be null;
     * @param testId  The test identifier that goes with the URL.  This may be null.
     * @return The work relative path of the JTR for this test.  Null if the
     * given URL is null.
     */
    public static String getWorkRelativePath(String baseURL, String testId) {
        StringBuilder sb = new StringBuilder(baseURL);

        // strip off extension
        stripExtn:
        for (int i = sb.length() - 1; i >= 0; i--) {
            switch (sb.charAt(i)) {
                case '.':
                    sb.setLength(i);
                    break stripExtn;
                case '/':
                    break stripExtn;
            }
        }

        // add in uniquifying id if
        if (testId != null) {
            sb.append('_');
            sb.append(testId);
        }

        sb.append(EXTN);

        return sb.toString();
    }

    /**
     * Check if this file is or appears to be a result (.jtr) file,
     * according to its filename extension.
     *
     * @param f the file to be checked
     * @return true if this file is or appears to be a result (.jtr) file.
     */
    public static boolean isResultFile(File f) {
        String p = f.getPath();
        return p.endsWith(EXTN);
    }

    /**
     * Parse the date format used for timestamps, such as the start/stop timestamp.
     *
     * @param s The string containing the date to be restored.
     * @see #formatDate
     */
    public static synchronized Date parseDate(String s) throws ParseException {
        return dateFormat.parse(s);
    }

    /**
     * Format the date format used for timestamps, such as the start/stop timestamp.
     *
     * @param d The date object to be formatted into a string.
     * @see #parseDate
     */
    public static synchronized String formatDate(Date d) {
        return dateFormat.format(d);
    }

    /**
     * @deprecated Use the Section API to accomplish your task.
     */
    @java.lang.Deprecated
    private static Reader getLastRefOutput(TestResult tr) {
        try {
            Section lastBlk = tr.getSection(tr.getSectionCount() - 1);
            return new StringReader(lastBlk.getOutput("ref"));
        } catch (ReloadFault f) {
            // not the best, but this method is deprecated and hopefully never
            // called
            return null;
        }
    }

    private static long computeChecksum(TestDescription td) {
        long cs = 0;
        for (Iterator<String> i = td.getParameterKeys(); i.hasNext(); ) {
            // don't rely on enumeration in a particular order
            // so simply add the checksum products together
            String key = i.next();
            cs += computeChecksum(key) * computeChecksum(td.getParameter(key));
        }
        return cs;
    }

    private static long computeChecksum(Section... sections) {
        long cs = sections.length;
        for (Section section : sections) {
            cs = cs * 37 + computeChecksum(section);
        }
        return cs;
    }

    private static long computeChecksum(Section s) {
        long cs = computeChecksum(s.getTitle());
        String[] names = s.getOutputNames();
        for (String name : names) {
            cs = cs * 37 + computeChecksum(name);
            cs = cs * 37 + computeChecksum(s.getOutput(name));
        }
        return cs;
    }

    private static long computeChecksum(String... strings) {
        long cs = strings.length;
        for (String string : strings) {
            cs = cs * 37 + computeChecksum(string);
        }
        return cs;
    }

    // ------------------------ OBSERVER MAINTENANCE -------------------------

    private static long computeChecksum(String s) {
        long cs = 0;
        for (int i = 0; i < s.length(); i++) {
            char c = s.charAt(i);
            //if (!Character.isISOControl(c) || c == '\n')
            cs = cs * 37 + c;
        }
        return cs;
    }

    private static boolean compare(Reader left, Reader right)
            throws Fault {
        try {
            try {
                for (; ; ) {
                    int l = left.read(), r = right.read();
                    if (l != r) {
                        return false; // different content found
                    }
                    if (l == -1) {
                        return true;
                    }
                }
            } finally {
                left.close();
                right.close();
            }
        } catch (IOException e) {
            throw new Fault(i18n, "rslt.badCompare", e);
        }
    }

    private static Status shareStatus(List<Map<String, Status>> tables, Status s) {
        int type = s.getType();
        String reason = s.getReason();
        Status result = tables.get(type).get(reason);
        if (result == null) {
            tables.get(type).put(reason, s);
            result = s;
        }

        return result;
    }

    /**
     * Create a new section inside this test result.
     *
     * @param name The symbolic name for this new section.
     * @return The new section that was created.
     */
    public synchronized TestResult.Section createSection(String name) {
        if (!isMutable()) {
            throw new IllegalStateException(
                    "This TestResult is no longer mutable!");
        }

        Section section = new Section(name);
        sections = DynamicArray.append(sections, section);
        notifyCreatedSection(section);
        // avoid creating output (which will cause observer messages)
        // before the createdSection has been notified
        section.createOutput(TestResult.MESSAGE_OUTPUT_NAME);

        return section;
    }

    /**
     * Add a new property value to this TestResult.
     *
     * @param name  The name of the property to be updated.
     * @param value The new value of the specified property.
     */
    public synchronized void putProperty(String name, String value) {
        // check mutability
        if (!isMutable()) {
            throw new IllegalStateException(
                    "Cannot put property, the TestResult is no longer mutable!");
        }

        props = PropertyArray.put(props, name, value);
        notifyUpdatedProperty(name, value);
    }

    /**
     * Sets the maximum output size for the current TestResult.
     * The value will be used instead of the value specified
     * by the system property javatest.maxOutputSize.
     *
     * @param size the maximum number of characters.
     */
    public synchronized void setMaxOutputSize(int size) {
        if (!isMutable()) {
            throw new IllegalStateException(
                    "This TestResult is no longer mutable!");
        }
        maxTROutputSize = size;
    }

    /**
     * Reconstruct the results of a previously run test.
     *
     * @param workDir Work directory in which the tests were run
     * @throws TestResult.Fault if an error occurs while reloading the results
     */
    public void reloadFromWorkDir(WorkDirectory workDir) throws Fault {
        // check mutability
        if (!isMutable()) {
            throw new IllegalStateException(
                    "Cannot reload results, the TestResult is no longer mutable!");
        }


        try {
            resultsFile = workDir.getFile(getWorkRelativePath());
            props = null;
            sections = null;
            execStatus = null;

            reload(new InputStreamReader(new FileInputStream(resultsFile), StandardCharsets.UTF_8));

            // this next line is dubious since the execStatus should have
            // been set during the reload
            execStatus = Status.parse(PropertyArray.get(props, EXEC_STATUS));
        } catch (FileNotFoundException e) {
            props = emptyStringArray;
            env = emptyStringArray;
            sections = emptySectionArray;
            execStatus = Status.notRun("no test result file found");
        } catch (IOException e) {
            props = emptyStringArray;
            env = emptyStringArray;
            sections = emptySectionArray;
            execStatus = Status.error("error opening result file: " + e);
            throw new Fault(i18n, "rslt.badFile", e.toString());
        } catch (Fault f) {
            props = emptyStringArray;
            env = emptyStringArray;
            sections = emptySectionArray;
            execStatus = Status.error(f.getMessage());
            throw f;
        }

    }

    /**
     * Get info about the checksum in this object.
     *
     * @return a value indicating the validity or otherwise of the checksum
     * found while reading this result object.
     * @see #NO_CHECKSUM
     * @see #BAD_CHECKSUM
     * @see #GOOD_CHECKSUM
     */
    public byte getChecksumState() {
        return checksumState;
    }

    /**
     * A way to write comments about the test execution into the results.
     *
     * @return If this is null, then the object is in a state in which it
     * does not accept new messages.
     */
    public PrintWriter getTestCommentWriter() {
        return sections[0].getMessageWriter();
    }

    /**
     * Get the test name, as given by the test URL defined by
     * TestDescription.getRootRelativeURL().  This method <em>always</em>
     * returns a useful string, representing the test name.
     *
     * @return the name of the test for which this is the result object
     * @see TestDescription#getRootRelativeURL
     */
    public String getTestName() {
        return testURL;
    }

    /**
     * Check whether this test result can be reloaded from a file.
     * This method does not validate the contents of the file.
     *
     * @return true if the result file for this object can be read
     */
    public boolean isReloadable() {
        return resultsFile != null && resultsFile.canRead();
    }

    /**
     * Check whether this object has been "shrunk" to reduce its
     * memory footprint. If it has, some or all of the data will have
     * to be reloaded.  This method is somewhat
     * orthogonal to {@code isReloadable()} and should not be used as a
     * substitute.
     *
     * @return True if this object is currently incomplete, false otherwise.
     * @see #isReloadable
     */
    public boolean isShrunk() {
        if (!isMutable() &&
                (desc == null ||
                        props == null ||
                        env == null ||
                        (sections == null && execStatus != inProgress))) {
            return true;
        } else {
            return false;
        }
    }

    /**
     * Get the description of the test from which this result was created.
     * Depending on how the test result was created, this information may
     * not be immediately available, and may be recreated from the test
     * result file.
     *
     * @return the test description for this test result object
     * @throws TestResult.Fault if there is a problem recreating the description
     *                          from the results file.
     */
    public synchronized TestDescription getDescription()
            throws Fault {
        if (desc == null) {
            // reconstitute description (probably from file)
            reload();
        }
        return desc;
    }

    /**
     * Get the path name for the results file for this test, relative to the
     * work directory.  The internal separator is '/'.
     *
     * @return the path name for the results file for this test,
     * relative to the work directory
     */
    public String getWorkRelativePath() {
        return getWorkRelativePath(testURL);
    }

    /**
     * Get the name, if any, for the result file for this object.
     * The path information contains platform specific path separators.
     *
     * @return the name, if any, for the result file for this object
     */
    public File getFile() {
        return resultsFile;
    }

    public void resetFile() {
        resultsFile = null;
    }

    /**
     * Get the keys of the properties that this object has stored.
     *
     * @return the keys of the properties that this object has stored
     */
    public synchronized Enumeration<String> getPropertyNames() {
        return PropertyArray.enumerate(props);
    }

    /**
     * Get the value of a property of this test result.
     *
     * @param name The name of the property to be retrieved.
     * @return The value corresponding to the property name, null if not
     * found.
     * @throws TestResult.Fault if there is a problem
     *                          recreating data from the results file.
     */
    public synchronized String getProperty(String name)
            throws Fault {
        if (props == null) {
            // reconstitute properties
            // this may result in a Fault, which is okay
            reload();
        }

        return PropertyArray.get(props, name);
    }

    /**
     * Get a copy of the environment that this object has stored.
     *
     * @return a copy of the environment that this object has stored
     * @throws TestResult.Fault if there is a problem
     *                          recreating data from the results file.
     * @see #setEnvironment
     */
    public synchronized Map<String, String> getEnvironment() throws Fault {
        if (env == null) {
            // reconstitute environment
            // this may result in a Fault, which is okay
            reload();
        }
        return PropertyArray.getProperties(env);
    }

    /**
     * Set the environment used by this test. When the test is run,
     * those entries in the environment that are referenced are noted;
     * those entries will be recorded here in the test result object.
     *
     * @param environment the test environment used by this test.
     * @see #getEnvironment
     */
    public synchronized void setEnvironment(TestEnvironment environment) {
        if (!isMutable()) {
            throw new IllegalStateException(
                    "This TestResult is no longer mutable!");
        }
        for (TestEnvironment.Element elem : environment.elementsUsed()) {
            // this is stunningly inefficient and should be fixed
            env = PropertyArray.put(env, elem.getKey(), elem.getValue());
        }
    }

    /**
     * Get the parent node in the test result table that
     * contains this test result object.
     *
     * @return the parent node in the test result table that
     * contains this test result object.
     */
    public TestResultTable.TreeNode getParent() {
        return parent;
    }

    /**
     * Set the parent node in the test result table that
     * contains this test result object.
     *
     * @param p the parent node in the test result table that
     *          contains this test result object.
     * @see #getParent
     */
    void setParent(TestResultTable.TreeNode p) {
        parent = p;
    }

    /**
     * Determine if the test result object is still mutable.
     * Test results are only mutable while they are being created, up to
     * the point that the final status is set.
     *
     * @return true if the test result object is still mutable,
     * and false otherwise
     */
    public synchronized boolean isMutable() {
        // must be mutable during reload (internal operation)
        // mutable as long as possible, to allow max time for writing log messages
        return execStatus == inProgress;
    }

    /**
     * Get the status for this test.
     *
     * @return the status for this test
     * @see #setStatus
     */
    public synchronized Status getStatus() {
        return execStatus;
    }

    /**
     * Set the result of this test.  This action makes this object immutable.
     * If a result comparison is needed, it will be done in here.
     *
     * @param stat A status object representing the outcome of the test
     * @see #getStatus
     */
    public synchronized void setStatus(Status stat) {
        if (!isMutable()) {
            throw new IllegalStateException(
                    "This TestResult is no longer mutable!");
        }

        if (stat == null) {
            throw new IllegalArgumentException(
                    "TestResult status cannot be set to null!");
        }

        // close out message section
        sections[0].setStatus(null);

        execStatus = stat;

        if (execStatus == inProgress) {
            execStatus = interrupted;
        }

        // verify integrity of status in all sections
        for (Section section : sections) {
            if (section.isMutable()) {
                section.setStatus(incomplete);
            }
        }

        props = PropertyArray.put(props, SECTIONS,
                StringArray.join(getSectionTitles()));
        props = PropertyArray.put(props, EXEC_STATUS,
                execStatus.toString());

        // end time now required
        // mainly for writing in the TRC for the Last Run Filter
        if (PropertyArray.get(props, END) == null) {
            props = PropertyArray.put(props, END, formatDate(new Date()));
        }

        // this object is now immutable
        notifyCompleted();
    }

    /**
     * Find out how many sections this test result contains.
     *
     * @return The number of sections in this result.
     */
    public synchronized int getSectionCount() {
        if (sections != null) {
            return sections.length;
        } else if (PropertyArray.get(props, SECTIONS) != null) {
            return parseSectionCount(PropertyArray.get(props, SECTIONS));
        } else {
            // hum, test props are never discarded, so we have no sections
            return 0;
        }
    }

    /**
     * Get the section specified by index.
     * Remember that index 0 is the default message section.
     *
     * @param index The index of the section to be retrieved.
     * @return The requested section.  Will be null if the section does not exist.
     * @throws TestResult.ReloadFault Will occur if an error is encountered when reloading
     *                                JTR data.  This may be the result of a corrupt or missing JTR file.
     * @see #MSG_SECTION_NAME
     */
    public synchronized Section getSection(int index) throws ReloadFault {
        Section target;

        if (sections == null && execStatus != inProgress) {
            // try to reload from file
            try {
                reload();
            } catch (ReloadFault f) {
                throw f;
            } catch (Fault f) {
                throw new ReloadFault(i18n, "rslt.badFile", f.getMessage());
            }
        }

        if (index >= sections.length) {
            target = null;
        } else {
            target = sections[index];
        }

        return target;
    }

    /**
     * Get the titles of all sections in this test result.
     * A null result probably indicates that there are no sections.  This is
     * improbable since most test result object automatically have one section.
     *
     * @return The titles, one at a time in the array.  Null if the titles
     * do not exist or could not be determined.
     */
    public synchronized String[] getSectionTitles() {
        if (props == null) {
            try {
                reload();
            } catch (Fault f) {
                // should this maybe be a JavaTestError?
                return null;
            }
        }

        // look for what we need from easiest to hardest source
        String names = PropertyArray.get(props, SECTIONS);

        if (names != null) {
            // it is cached
            return StringArray.split(names);
        } else if (sections != null) {
            // TR is not immutable yet, probably
            int numSections = getSectionCount();
            String[] data = new String[numSections];

            for (int i = 0; i < numSections; i++) {
                data[i] = sections[i].getTitle();
            }

            return data;
        } else {
            // hum, bad.  No sections exist and this data isn't cached
            // the test probably has not run
            return null;
        }
    }

    /**
     * Writes the TestResult into a version 2 jtr file.
     *
     * @param workDir      The work directory in which to write the results
     * @param backupPolicy a policy object defining what to do if a file
     *                     already exists with the same name as that which is about to be written.
     * @throws IllegalStateException This will occur if you attempt to write a result
     *                               which is still mutable.
     * @throws IOException           Occurs when the output file cannot be created or written to.
     *                               Under this condition, this object will change it status to an error.
     */
    public synchronized void writeResults(WorkDirectory workDir, BackupPolicy backupPolicy)
            throws IOException {
        if (isMutable()) {
            throw new IllegalStateException("This TestResult is still mutable - set the status!");
        }

        // could attempt a reload() I suppose
        if (props == null) {
            props = emptyStringArray;
        }

        String wrp = getWorkRelativePath(desc).replace('/', File.separatorChar);
        resultsFile = workDir.getFile(wrp);

        File resultsDir = resultsFile.getParentFile();
        resultsDir.mkdirs(); // ensure directory created for .jtr file

        File tempFile = createTempFile(workDir, backupPolicy);
        try {
            writeResults(tempFile, backupPolicy);
        } finally {
            if (tempFile.exists()) {
                tempFile.delete();
            }
        }
    }

    /**
     * Create a temporary file to which the results can be written, before being renamed
     * to its real name.
     */
    // don't use File.createTempFile because of issues with the internal locking there
    private File createTempFile(WorkDirectory workDir, BackupPolicy backupPolicy)
            throws IOException {
        int MAX_TRIES = 100; // absurdly big limit, but a limit nonetheless
        for (int i = 0; i < MAX_TRIES; i++) {
            File tempFile = new File(resultsFile.getPath() + "." + i + ".tmp");
            if (tempFile.createNewFile()) {
                return tempFile;
            }
        }
        throw new IOException("could not create temp file for " + resultsFile + ": too many tries");
    }

    /**
     * Write the results to a temporary file, and when done, rename it to resultsFile
     */
    private void writeResults(File tempFile, BackupPolicy backupPolicy)
            throws IOException {
        Writer out;
        try {
            out = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(tempFile), StandardCharsets.UTF_8));
        } catch (IOException e) {
            execStatus = Status.error("Problem writing result file for test: " + getTestName());
            resultsFile = null; // file not successfully written after all
            throw e;
        }

        try {
            // redundant, is done in setResult
            // needed though if setResult isn't being called
            props = PropertyArray.put(props, EXEC_STATUS, execStatus.toString());

            // file header
            out.write(JTR_V2_HEADER);
            out.write(lineSeparator);

            // date and time
            out.write("#" + new Date().toString());
            out.write(lineSeparator);

            // checksum header and data
            //out.write(JTR_V2_CHECKSUM);
            //out.write(Long.toHexString(computeChecksum()));
            //out.write(lineSeparator);
 /*
            if (debug) {  // debugging code
                out.write("# debug: test desc checksum: ");
                out.write(Long.toHexString(computeChecksum(desc)));
                out.write(lineSeparator);

                for (Iterator iter = desc.getParameterKeys(); iter.hasNext(); ) {
                    // don't rely on enumeration in a particular order
                    // so simply add the checksum products together
                    String KEY = (String) (iter.next());
                    out.write("# debug: test desc checksum key " + KEY + ": ");
                    out.write(Long.toHexString(computeChecksum(KEY) * computeChecksum(desc.getParameter(KEY))));
                    out.write(lineSeparator);
                }

                out.write("# debug: test env checksum: ");
                if (env == null)
                    out.write("null");
                else
                    out.write(Long.toHexString(computeChecksum(env)));
                out.write(lineSeparator);

                out.write("# debug: test props checksum: ");
                out.write(Long.toHexString(computeChecksum(props)));
                out.write(lineSeparator);

                out.write("# debug: test sections checksum: ");
                out.write(Long.toHexString(computeChecksum(sections)));
                out.write(lineSeparator);

                for (int I = 0; I < sections.length; I++) {
                    out.write("# debug: test section[" + I + "] checksum: ");
                    out.write(Long.toHexString(computeChecksum(sections[I])));
                    out.write(lineSeparator);

                    String[] NAMES = sections[I].getOutputNames();
                    for (int J = 0; J < NAMES.length; J++) {
                        out.write("# debug: test section[" + I + "] name=" + NAMES[J] + " checksum: ");
                        out.write(Long.toHexString(computeChecksum(NAMES[J])));
                        out.write(lineSeparator);

                        out.write("# debug: test section[" + I + "] name=" + NAMES[J] + " output checksum: ");
                        out.write(Long.toHexString(computeChecksum(sections[I].getOutput(NAMES[J]))));
                        out.write(lineSeparator);
                    }
                }
            }*/

            // description header and data
            out.write(JTR_V2_TESTDESC);
            out.write(lineSeparator);

            Map<String, String> tdProps = new HashMap<>();
            desc.save(tdProps);
            PropertyArray.save(PropertyArray.getArray(tdProps), out);
            out.write(lineSeparator);

            // test environment header and data
            if (env != null) {
                out.write(JTR_V2_ENVIRONMENT);
                out.write(lineSeparator);
                PropertyArray.save(env, out);
                out.write(lineSeparator);
            }

            // test result props header and data
            out.write(JTR_V2_RESPROPS);
            out.write(lineSeparator);
            PropertyArray.save(props, out);
            out.write(lineSeparator);

            // get sections into memory
            // I hope the out stream is not the same as the resultFile!
            if (sections == null) {
                throw new JavaTestError("Cannot write test result - it contains no sections.");
            }

            for (Section section : sections) {
                section.save(out);
            }

            out.write(lineSeparator);
            out.write(JTR_V2_TSTRESULT);
            out.write(execStatus.toString());
            out.write(lineSeparator);
            out.close();
        }   // try
        catch (IOException e) {
            // This exception could be raised when trying to create the directory
            // for the test results; opening the results file, or closing it.
            execStatus = Status.error("Write to temp. JTR file failed (old JTR intact): " +
                    tempFile.getPath());
            resultsFile = null; // file not successfully written after all
            throw e;
        }   // catch

        try {
            backupPolicy.backupAndRename(tempFile, resultsFile);

            // now that it has been successfully written out, make the object
            // a candidate for shrinking
            addToShrinkList();
        }   // try
        catch (IOException e) {
            // This exception could be raised when trying to create the directory
            // for the test results; opening the results file, or closing it.
            execStatus = Status.error("Problem writing result file: " +
                    resultsFile.getPath());
            resultsFile = null; // file not successfully written after all
            throw e;
        }   // catch
    }

    /**
     * Add an observer to watch this test result for changes.
     *
     * @param obs the observer to be added
     */
    public synchronized void addObserver(Observer obs) {
        if (isMutable()) {
            Observer[] observers = observersTable.get(this);

            if (observers == null) {
                observers = new Observer[0];
            }

            observers = DynamicArray.append(observers, obs);
            observersTable.put(this, observers);
        }
    }

    /**
     * Remove an observer that was previously added.
     *
     * @param obs the observer to be removed
     */
    public synchronized void removeObserver(Observer obs) {
        Observer[] observers = observersTable.get(this);
        if (observers == null) {
            return;
        }

        observers = DynamicArray.remove(observers, obs);
        if (observers == null) {
            observersTable.remove(this);
        } else {
            observersTable.put(this, observers);
        }
    }

    /**
     * Gets the time when the test was completed, or at least the time
     * when it's final status was set.  Be aware that if the information is
     * not available in memory, it will be loaded from disk.
     *
     * @return Time when this test acquired its final status setting.
     * @see #setStatus
     * @see java.util.Date
     */
    public long getEndTime() {
        if (endTime < 0) {
            try {
                String datestr = PropertyArray.get(props, END);

                if (datestr == null) {
                    // this may be more expensive because it can cause a
                    // reload from disk
                    try {
                        datestr = getProperty(END);
                    } catch (Fault f) {
                    }
                }

                if (datestr != null) {
                    Date date = parseDate(datestr);
                    endTime = date.getTime();
                } else {
                    // info not available
                }
            } catch (ParseException e) {
            }
        }

        return endTime;
    }

    void shareStatus(List<Map<String, Status>> tables) {
        execStatus = shareStatus(tables, execStatus);
    }

    String[] getTags() {
        // Script or someone else could possibly do this w/the observer
        if (sections == null) {
            return null;
        }

        Vector<String> tagV = new Vector<>(sections.length * 2);

        for (Section section : sections) {
            String[] names = section.getOutputNames();

            Collections.addAll(tagV, names);
        } // outer for

        return tagV.toArray(new String[tagV.size()]);
    }

    /**
     * Insert a test description into this test results.
     * This will only work if the test description is currently not available.
     * The name in the test description must match the name of this test.
     *
     * @param td The new test description, a null value will have no effect.
     * @throws IllegalStateException If the state of this object fobiu
     * @see #isShrunk()
     */
    void setTestDescription(TestDescription td) {
        if (td == null) {
            return;
        }

        String name = td.getRootRelativeURL();
        if (!testURL.equals(name)) {
            throw new IllegalStateException();
        }

        if (desc != null) {             // compare if possible
            if (!desc.equals(td)) {     // test descriptions are not the same
                // accept new TD, reset this TR
                // reset status to a special one
                execStatus = tdMismatch;
                desc = td;

                props = emptyStringArray;
                resultsFile = null;
                env = emptyStringArray;
                sections = emptySectionArray;

                if (isMutable()) {
                    createSection(MSG_SECTION_NAME);
                }
            } else {
                // TDs are equal, no action, drop thru and return
            }
        } else {
            desc = td;
        }
    }

    private long computeChecksum() {
        long cs = 0;
        cs = cs * 37 + computeChecksum(desc);
        // in JT2.1.1a, environment was not included in checksum,
        // so allow that for backward compatibility
        String jtv = PropertyArray.get(props, VERSION);
        if (env != null) {
            if (jtv == null || !jtv.equals("JT_2.1.1a")) {
                cs = cs * 37 + computeChecksum(env);
            }
        }
        cs = cs * 37 + computeChecksum(props);
        if (sections != null) {
            cs = cs * 37 + computeChecksum(sections);
        }
        cs = cs * 37 + execStatus.getType() + computeChecksum(execStatus.getReason());
        return Math.abs(cs);  // ensure +ve, to avoid sign issues!
    }

    /**
     * @throws ResultFileNotFoundFault May be thrown if the JTR file cannot be found.
     * @throws ReloadFault             Generally describes any error which is encountered while
     *                                 reading or processing the input file.
     */
    private synchronized void reload()
            throws ResultFileNotFoundFault, ReloadFault {
        if (resultsFile == null) {
            throw new ReloadFault(i18n, "rslt.noResultFile");
        }

        if (isMutable()) {
            throw new IllegalStateException("Cannot do a reload of this object.");
        }

        try {
            reload(new BufferedReader(new InputStreamReader(new FileInputStream(resultsFile), StandardCharsets.UTF_8)));

            // Well, we have successfully reloaded it, so the object is now taking
            // up a big footprint again ... put it back on the list to be shrunk again
            addToShrinkList();
        } catch (FileNotFoundException e) {
            throw new ResultFileNotFoundFault(i18n, "rslt.fileNotFound", resultsFile);
        } catch (IOException e) {
            throw new ReloadFault(i18n, "rslt.badFile", e);
        }
    }

    /**
     * @throws ReloadFault Generally describes any error which is encountered while
     *                     reading or processing the input file.  This may indicate
     *                     an empty file or incorrectly formatted file.
     */
    private void reload(Reader r)
            throws ReloadFault, IOException {
        try {
            BufferedReader br = new BufferedReader(r);
            String line = br.readLine();

            // determine JTR version
            if (line == null) {
                throw new ReloadFault(i18n, "rslt.empty", resultsFile);
            }
            if (line.equals(JTR_V2_HEADER)) {
                reloadVersion2(br);
            } else if (line.equals(JTR_V1_HEADER)) {
                reloadVersion1(br);
            } else {
                throw new ReloadFault(i18n, "rslt.badHeader", resultsFile);
            }
        } catch (RuntimeException e) {
            throw new ReloadFault(i18n, "rslt.badRuntimeErr", resultsFile.getPath(), e.getLocalizedMessage());
        } finally {
            r.close();
        }
    }

    private void reloadVersion1(BufferedReader in)
            throws ReloadFault, IOException {
        // grab property info
        StringBuffer buff = new StringBuffer();
        String line = in.readLine();
        while (!(line == null) && !(line.isEmpty())) {
            buff.append(line);
            buff.append(lineSeparator);
            line = in.readLine();
        }

        // store if needed
        Map<String, String> pairs = new HashMap<>();
        if (props == null || desc == null) {
            StringReader sr = new StringReader(buff.toString());
            buff = null;
            line = null;

            pairs = PropertyUtils.load(sr);
        }

        if (props == null) {
            // reload test result properties
            props = PropertyArray.getArray(pairs);
        }

        pairs = null;

        if (desc == null) {
            File path = new File(PropertyArray.get(props, "testsuite"));
            if (!path.isDirectory()) {
                path = new File(path.getParent());
            }
            File file = new File(PropertyArray.get(props, "file"));

            uniquifyStrings(props);

            desc = new TestDescription(path, file,
                    PropertyArray.getProperties(props));
        }

        buff = new StringBuffer();
        line = in.readLine();
        while (!(line == null)) {
            if (line.startsWith("command: ")) {
                // a section
                Section blk = processOldSection(line, in);

                if (blk != null) {
                    sections = DynamicArray.append(sections, blk);
                }
            } else if (line.startsWith(JTR_V1_TSTRESULT)) {
                // test result
                if (line == null) {
                    // couldn't get the status text for some reason
                } else {
                    line = extractSlice(line, JTR_V1_TSTRESULT.length(), " ", null);
                    execStatus = Status.parse(line);
                }   // inner else

                break;
            } else {
                // message text
                buff.append(line);
                buff.append(lineSeparator);
            }   // else

            line = in.readLine();
        }   // while

        // create the test message section and put first in the array
        Section blk = new Section(MSG_SECTION_NAME);
        blk.reloadOutput(MESSAGE_OUTPUT_NAME, buff.toString());
        Section[] tempBlks = new Section[sections.length + 1];
        tempBlks[0] = blk;
        System.arraycopy(sections, 0, tempBlks, 1, sections.length);
        sections = tempBlks;
    }

    private Section processOldSection(String line1, BufferedReader in)
            throws ReloadFault, IOException {
        StringBuilder sb = new StringBuilder();         // message stream
        Section section = null;
        String line = line1;
        while (!(line == null)) {
            if (line.startsWith("----------")) {
                String streamName = null;
                String sectionName = null;
                StringBuilder buff = new StringBuilder();
                int lines = 0;
                int chars = 0;
                try {
                    streamName = extractSlice(line, 10, null, ":");
                    sectionName = extractSlice(line, 10, ":", "(");
                    lines = Integer.parseInt(extractSlice(line, 10, "(", "/"));
                    chars = Integer.parseInt(extractSlice(line, 10, "/", ")"));

                    for (int count = 0; count < lines; count++) {
                        buff.append(in.readLine());
                    }
                } catch (NumberFormatException e) {
                    // confused!
                    throw new ReloadFault(i18n, "rslt.badFile", e);
                }

                if (section == null) {
                    section = new Section(sectionName);
                }

                section.reloadOutput(streamName, buff.toString());
            } else if (line.startsWith(JTR_V1_SECTRESULT)) {
                // set result
                if (section == null) {
                    section = new Section("");
                }

                // get the Status text
                line = extractSlice(line, JTR_V1_SECTRESULT.length(), " ", null);

                if (line == null)
                // couldn't get the status text for some reason
                {
                    throw new ReloadFault(i18n, "rslt.noSectionResult");
                } else {
                    section.reloadStatus(Status.parse(line));
                }

                break;
            } else {
                // just a plain message
                sb.append(line);
                sb.append(lineSeparator);
            }

            line = in.readLine();
        }

        if (section != null) {
            section.reloadOutput(MESSAGE_OUTPUT_NAME, sb.toString());
        }

        return section;
    }

    private void reloadVersion2(BufferedReader in)
            throws ReloadFault, IOException {
        //String checksumText = null;
        String line;

        // look for optional checksum and then test description,
        // skipping comments
        while ((line = in.readLine()) != null) {
            if (line.equals(JTR_V2_TESTDESC)) {
                break;
            }
            //else if (line.startsWith(JTR_V2_CHECKSUM)) {
            //checksumText = line.substring(JTR_V2_CHECKSUM.length());
            //}
            else if (!line.startsWith("#")) {
                throw new ReloadFault(i18n, "rslt.badLine", line);
            }
        }

        // this probably won't work with a normal Properties object
        String[] tdProps = PropertyArray.load(in);

        if (desc == null) {
            uniquifyStrings(tdProps);
            desc = TestDescription.load(tdProps);
        }
        tdProps = null;                // dump it

        // XXX compare to TD

        // remove comment lines and look for test env props
        while ((line = in.readLine()) != null) {
            if (line.startsWith(JTR_V2_RESPROPS)) {
                break;
            } else if (line.startsWith(JTR_V2_ENVIRONMENT)) {
                env = PropertyArray.load(in);
                uniquifyStrings(env);
            } else if (!line.startsWith("#")) {
                throw new ReloadFault(i18n, "rslt.badLine", line);
            }
        }

        if (env == null) {
            env = new String[]{};
        }

        if (line == null) {
            throw new ReloadFault(i18n, "rslt.badFormat");
        }

        String[] trProps = PropertyArray.load(in);

        if (props == null) {
            // restore the properties of this result
            uniquifyStrings(trProps);
            props = trProps;
        }

        trProps = null;             // dump it

        // read the sections
        int sectionCount = parseSectionCount(PropertyArray.get(props, SECTIONS));
        sections = new Section[sectionCount];
        for (int i = 0; i < getSectionCount(); i++) {
            sections[i] = new Section(in);
        }

        // get the final test status
        while ((line = in.readLine()) != null) {
            if (line.startsWith(JTR_V2_TSTRESULT)) {
                execStatus = Status.parse(line.substring(JTR_V2_TSTRESULT.length()));
                break;
            }
        }

        if (execStatus == null) {
            execStatus = Status.error("NO STATUS RECORDED IN FILE");
        }

        // checksum support removed
        checksumState = NO_CHECKSUM;
    }

    /**
     * This method tolerates null.  It expects a list of section names - basically
     * a space separated list and returns the number of items there.
     *
     * @param s The section name list string to parse and count.  May be null.
     * @return Number of sections listed in the string.  Will be zero if the
     * input was null.
     */
    int parseSectionCount(String s) {
        if (s == null || s.isEmpty()) {
            return 0;
        }

        return StringArray.split(s).length;
    }

    void uniquifyStrings(String... data) {
        for (int i = 0; i < data.length; i++)
        // don't do this for large strings
        {
            if (data[i] != null && data[i].length() < 30) {
                data[i] = data[i].intern();
            }
        }
    }

    /**
     * Extract a substring specified by a start and end pattern (string).
     * The start and end strings must be single chars.
     *
     * @param s     String to do this operation on
     * @param where Position in the string to start at
     * @param start Beginning pattern for the slice, exclusive.
     * @param end   Ending pattern for the slice, exclusive.  Null means
     *              to-end-of-string.
     * @return The requested substring or null if error.
     */
    String extractSlice(String s, int where, String start, String end) {
        int startInd;
        int endInd;

        if (start == null) {
            startInd = where;
        } else {
            int i = s.indexOf(start, where);
            if (i < 0) {
                return null;
            }
            startInd = i + start.length();
        }

        if (end == null) {
            endInd = s.length();
        } else {
            endInd = s.indexOf(end, startInd);
            if (endInd == -1) {
                return null;
            }
        }

        try {
            return s.substring(startInd, endInd);
        } catch (StringIndexOutOfBoundsException e) {
            return null;
        }
    }

    /**
     * Notify observers that a new section has been created.
     *
     * @param section The section that was created.
     */
    private synchronized void notifyCreatedSection(Section section) {
        Observer[] observers = observersTable.get(this);
        if (observers != null) {
            for (Observer observer : observers) {
                observer.createdSection(this, section);
            }
        }
    }

    /**
     * Notify observers that a section has been completed.
     *
     * @param section The section that was completed.
     */
    private synchronized void notifyCompletedSection(Section section) {
        Observer[] observers = observersTable.get(this);
        if (observers != null) {
            for (Observer observer : observers) {
                observer.completedSection(this, section);
            }
        }
    }

    /**
     * Notify observers that new output is being created.
     *
     * @param section    The section that was created.
     * @param outputName The name of the output.
     */
    private synchronized void notifyCreatedOutput(Section section, String outputName) {
        Observer[] observers = observersTable.get(this);
        if (observers != null) {
            for (Observer observer : observers) {
                observer.createdOutput(this, section, outputName);
            }
        }
    }

    /**
     * Notify observers that a particular output has been completed.
     *
     * @param section    The section that was completed.
     * @param outputName The name of the output.
     */
    private synchronized void notifyCompletedOutput(Section section, String outputName) {
        Observer[] observers = observersTable.get(this);
        if (observers != null) {
            for (Observer observer : observers) {
                observer.completedOutput(this, section, outputName);
            }
        }
    }

    /**
     * Notify all observers that new data has been written to some output.
     *
     * @param section    The section being modified.
     * @param outputName The stream of the section that is being modified.
     * @param text       The text that was added (appended).
     */
    private synchronized void notifyUpdatedOutput(Section section, String outputName, int start, int end, String text) {
        Observer[] observers = observersTable.get(this);
        if (observers != null) {
            for (Observer observer : observers) {
                observer.updatedOutput(this, section, outputName, start, end, text);
            }
        }
    }

    /**
     * Notify all observers that new data has been written to some output.
     *
     * @param section    The section being modified.
     * @param outputName The stream of the section that is being modified.
     */
    private synchronized void notifyUpdatedOutput(Section section, String outputName, int start, int end,
                                                  char[] buf, int offset, int len) {
        Observer[] observers = observersTable.get(this);
        if (observers != null) {
            // only create string if there are really observers who want to see it
            String text = new String(buf, offset, len);
            for (Observer observer : observers) {
                observer.updatedOutput(this, section, outputName, start, end, text);
            }
        }
    }

    /**
     * Notify all observers that a property has been updated.
     *
     * @param key   The key for the property that was modified.
     * @param value The new value for the property.
     */
    private synchronized void notifyUpdatedProperty(String key, String value) {
        Observer[] observers = observersTable.get(this);
        if (observers != null) {
            for (Observer observer : observers) {
                observer.updatedProperty(this, key, value);
            }
        }
    }

    /**
     * Notify observers the test has completed.
     */
    private synchronized void notifyCompleted() {
        // since there will be no more observer messages after this, there
        // is no need to keep any observers registered after we finish here
        // so get the observers one last time, and at the same time
        // remove them from the table
        Observer[] observers = observersTable.remove(this);
        if (observers != null) {
            for (Observer observer : observers) {
                observer.completed(this);
            }
            observersTable.remove(this);
        }

    }

    /**
     * @return Position of the specified section, or -1 if not found.
     */
    private synchronized int findSection(String name) {
        int location;

        if (sections == null || sections.length == 0) {
            return -1;
        }

        for (location = 0; location < sections.length; location++) {
            if (sections[location].getTitle().equals(name)) {
                // found
                break;
            }
        }   // for

        // loop exited because of counter, not a hit
        if (location == sections.length) {
            location = -1;
        }

        return location;
    }

    private void addToShrinkList() {
        synchronized (shrinkList) {
            // if this object is in the list; remove it;
            // if there are dead weak refs, remove them
            for (Iterator<WeakReference<TestResult>> iter = shrinkList.iterator(); iter.hasNext(); ) {
                WeakReference<TestResult> wref = iter.next();
                Object o = wref.get();
                if (o == null || o == this) {
                    iter.remove();
                }
            }
            while (shrinkList.size() >= maxShrinkListSize) {
                WeakReference<TestResult> wref = shrinkList.removeFirst();
                TestResult tr = wref.get();
                if (tr != null) {
                    tr.shrink();
                }
            }
            shrinkList.addLast(new WeakReference<>(this));
        }
    }

    /**
     * Tells the object that it can optimize itself for a small memory footprint.
     * Doing this may sacrifice performance when accessing object data.  This
     * only works on results that are immutable.
     */
    private synchronized void shrink() {
        if (isMutable()) {
            throw new IllegalStateException("Can't shrink a mutable test result!");
        }

        // Should ensure we have a resultsFile.
        sections = null;

        // NOTE: if either of these are discarded, it may be a good idea to
        //       optimize reload() to not read the section/stream data since
        //       a small property lookup could incur a huge overhead
        //props = null;         // works, may or may-not improve memory usage
        //desc = null;          // doesn't work in current implementation
    }

    /**
     * An interface to observe activity in a TestResult as it is created.
     */
    public interface Observer {
        /**
         * A new section has been created in the test result.
         *
         * @param tr      The test result in which the section was created.
         * @param section The section that has been created
         */
        void createdSection(TestResult tr, Section section);

        /**
         * A section has been been completed in the test result.
         *
         * @param tr      The test result containing the section.
         * @param section The section that has been completed.
         */
        void completedSection(TestResult tr, Section section);

        /**
         * New output has been created in a section of the test result.
         *
         * @param tr         The test result containing the output.
         * @param section    The section in which the output has been created.
         * @param outputName The name of the output.
         */
        void createdOutput(TestResult tr, Section section, String outputName);

        /**
         * Output has been completed in a section of the test result.
         *
         * @param tr         The test result containing the output.
         * @param section    The section in which the output has been completed.
         * @param outputName The name of the output.
         */
        void completedOutput(TestResult tr, Section section, String outputName);

        /**
         * The output for a section has been updated.
         *
         * @param tr         The test result object being modified.
         * @param section    The section in which the output is being produced.
         * @param outputName The name of the output.
         * @param start      the start offset of the text that was changed
         * @param end        the end offset of the text that was changed
         * @param text       the text that replaced the specified range.
         */
        void updatedOutput(TestResult tr, Section section, String outputName, int start, int end, String text);

        /**
         * A property of the test result has been updated.
         *
         * @param tr    The test result containing the property that was modified.
         * @param name  The key for the property that was modified.
         * @param value The new value for the property.
         */
        void updatedProperty(TestResult tr, String name, String value);

        /**
         * The test has completed, and the results are now immutable.
         * There will be no further observer calls.
         *
         * @param tr The test result that has been completed.
         */
        void completed(TestResult tr);

    }
    // Conceptually, this belongs in Section, but that is not legal Java.
    // (It is accepted in  1.1.x; rejected by 1.2)
    private interface OutputBuffer {
        String getName();

        String getOutput();

        PrintWriter getPrintWriter();
    }

    @Override
    public String toString() {
        return "TestResult{" +
                "resultsFile=" + resultsFile +
                ", execStatus=" + execStatus +
                ", testURL='" + testURL + '\'' +
                ", endTime=" + endTime +
                ", checksumState=" + checksumState +
                ", desc=" + desc +
                ", props=" + Arrays.toString(props) +
                ", env=" + Arrays.toString(env) +
                ", sections=" + Arrays.toString(sections) +
                ", maxTROutputSize=" + maxTROutputSize +
                ", parent=" + parent +
                '}';
    }

    /**
     * This exception is to report problems using TestResult objects.
     */
    public static class Fault extends Exception {
        Fault(I18NResourceBundle i18n, String key) {
            super(i18n.getString(key));
        }

        Fault(I18NResourceBundle i18n, String key, Object arg) {
            super(i18n.getString(key, arg));
        }

        Fault(I18NResourceBundle i18n, String key, Object... args) {
            super(i18n.getString(key, args));
        }
    }

    /**
     * This exception is thrown if the JTR file cannot be found.
     */
    public static class ResultFileNotFoundFault extends Fault {
        ResultFileNotFoundFault(I18NResourceBundle i18n, String key) {
            super(i18n, key);
        }

        ResultFileNotFoundFault(I18NResourceBundle i18n, String key, Object arg) {
            super(i18n, key, arg);
        }

        ResultFileNotFoundFault(I18NResourceBundle i18n, String key, Object... args) {
            super(i18n, key, args);
        }
    }

    /**
     * This exception ay occur anytime the JTR file is being read from the filesystem.
     * To optimize memory usage, the contents of a TestResult object are sometimes
     * discarded and then loaded on demand from the JTR file.  If a fault occurs
     * when reading the JTR file, this fault may occur.
     *
     * @see TestResult.ResultFileNotFoundFault
     */
    public static class ReloadFault extends Fault {
        ReloadFault(I18NResourceBundle i18n, String key) {
            super(i18n, key);
        }

        ReloadFault(I18NResourceBundle i18n, String key, Object arg) {
            super(i18n, key, arg);
        }

        ReloadFault(I18NResourceBundle i18n, String key, Object... args) {
            super(i18n, key, args);
        }
    }

    private static class LockedWriter extends PrintWriter {
        public LockedWriter(Writer out, Object theLock) {
            super(out);
            lock = theLock;
        }
    }

    /**
     * This "section" is the logical combination of a single action during test
     * execution.  It is designed to hold multiple (or none) buffers of
     * output from test execution, such as stdout and stderr.  In addition,
     * it has a "comment" field for tracking the test run itself (progress).
     * This output is identified by the MSG_SECTION_NAME identifier.
     */
    public class Section {
        private OutputBuffer[] buffers = new OutputBuffer[0];
        private String title;
        private Status result;

        public Section(String title) {
            if (title == null) {
                throw new NullPointerException();
            }
            if (title.indexOf(' ') != -1) {
                throw new IllegalArgumentException("space invalid in section title");
            }

            this.title = title;
            result = inProgress;
        }

        /**
         * Could be used to reconstruct the section from a stream.
         * Reads from the source until it finds a section header.  This is a JTR
         * version 2 method, don't use it for version 1 files.  The object
         * immediately immutable upon return from this constructor.
         *
         * @throws ReloadFault Probably an error while parsing the input stream.
         */
        Section(BufferedReader in) throws IOException, ReloadFault {
            String line = in.readLine();
            // find top of section and process it
            while (line != null) {
                if (line.startsWith(JTR_V2_SECTION)) {
                    title = extractSlice(line, 0, ":", null);
                    break;
                } else
                // don't know what this line is, may be empty
                {
                    line = in.readLine();
                }
            }

            if (title == null) {
                throw new ReloadFault(i18n, "rslt.noSectionTitle");
            }

            if (title.equals(MSG_SECTION_NAME)) {
                // use standard internal copy of string
                title = MSG_SECTION_NAME;
            }

            while ((line = in.readLine()).startsWith(JTR_V2_SECTSTREAM)) {
                OutputBuffer b = new FixedOutputBuffer(line, in);
                buffers = DynamicArray.append(buffers, b);
            }

            // if not in the message section, line should have the section result
            if (!Objects.equals(title, MSG_SECTION_NAME)) {
                if (line != null) {
                    if (line.startsWith(JTR_V2_SECTRESULT)) {
                        result = Status.parse(line.substring(JTR_V2_SECTRESULT.length()));
                    } else {
                        throw new ReloadFault(i18n, "rslt.badLine", line);
                    }
                }
                if (result == null)
                // no test result
                {
                    throw new ReloadFault(i18n, "rslt.noSectionResult");
                }
            }
        }

        /**
         * Query if the section is still writable or not.
         *
         * @return true if the section is still writable, and false otherwise
         */
        public boolean isMutable() {
            synchronized (TestResult.this) {
                synchronized (this) {
                    return TestResult.this.isMutable() &&
                            this.result == inProgress;
                }
            }
        }

        /**
         * Find out what the result of the execution of this section was.
         *
         * @return the result of the execution of this section
         * @see #setStatus
         */
        public Status getStatus() {
            return result;
        }

        /**
         * Set the result of this section.  This action makes this section
         * immutable.
         *
         * @param result The status to set as the result of this section of the test
         * @see #getStatus
         */
        public void setStatus(Status result) {
            synchronized (TestResult.this) {
                synchronized (this) {
                    checkMutable();
                    for (OutputBuffer b : buffers) {
                        if (b instanceof WritableOutputBuffer) {
                            WritableOutputBuffer wb = (WritableOutputBuffer) b;
                            wb.getPrintWriter().close();
                        }
                    }
                    if (env == null) {
                        env = emptyStringArray;
                    }
                    this.result = result;
                    if (env == null) {
                        env = emptyStringArray;
                    }
                    notifyCompletedSection(this);
                }
            }
        }

        /**
         * Get the title of this section, specified when the section
         * was created.
         *
         * @return the title of this section
         */
        public String getTitle() {
            return title;
        }

        /**
         * Get the appropriate to writer to access the default message field.
         *
         * @return a Writer to access the default message field
         */
        public PrintWriter getMessageWriter() {
            synchronized (TestResult.this) {
                synchronized (this) {
                    checkMutable();
                    // if it is mutable, it must have a message stream,
                    // which will be the first entry
                    return buffers[0].getPrintWriter();
                }
            }
        }

        // ---------- PACKAGE PRIVATE ----------

        /**
         * Find out how many output buffers this section has inside it.
         *
         * @return The number of output buffers in use (&gt;=0).
         */
        public synchronized int getOutputCount() {
            return buffers.length;
        }

        /**
         * Add a new output buffer to the section; get PrintWriter access to it.
         *
         * @param name The symbolic name that will identify this new stream.
         * @return A PrintWriter that gives access to the new stream.
         */
        public PrintWriter createOutput(String name) {
            if (name == null) {
                throw new NullPointerException();
            }

            synchronized (TestResult.this) {
                synchronized (this) {
                    checkMutable();

                    OutputBuffer b = new WritableOutputBuffer(name);
                    buffers = DynamicArray.append(buffers, b);

                    notifyCreatedOutput(this, name);

                    return b.getPrintWriter();
                }
            }
        }

        /**
         * Get the content that was written to a specified output stream.
         *
         * @param name the name of the stream in question
         * @return All the data that was written to the specified output,
         * or null if nothing has been written.
         */
        public String getOutput(String name) {
            if (name == null) {
                throw new NullPointerException();
            }

            synchronized (TestResult.this) {
                synchronized (this) {
                    OutputBuffer b = findOutputBuffer(name);
                    return b == null ? null : b.getOutput();
                }
            }
        }

        /**
         * Find out the symbolic names of all the streams in this section.  You
         * can use getOutputCount to discover the number of items in
         * this enumeration (not a thread safe activity in the strictest
         * sense of course).
         *
         * @return A list of strings which are the symbolic names of the streams in this section.
         * @see #getOutputCount
         */
        public synchronized String[] getOutputNames() {
            String[] names = new String[buffers.length];

            for (int i = 0; i < buffers.length; i++) {
                names[i] = buffers[i].getName();
                if (names[i] == null) {
                    throw new IllegalStateException("BUFFER IS BROKEN");
                }
            }

            return names;
        }

        /**
         * Removes any data added to the named output up to this point, resetting
         * it to an empty state.
         *
         * @param name The output name to erase the content of.
         * @since 4.2.1
         */
        public synchronized void deleteOutputData(String name) {
            if (name == null) {
                throw new NullPointerException();
            }

            synchronized (TestResult.this) {
                synchronized (this) {
                    OutputBuffer b = findOutputBuffer(name);
                    if (b != null && b instanceof WritableOutputBuffer) {
                        ((WritableOutputBuffer) b).deleteAllOutput();
                    }
                }
            }
        }

        // ---------- PRIVATE ----------

        void save(Writer out) throws IOException {
            out.write(JTR_V2_SECTION + getTitle());
            out.write(lineSeparator);

            for (OutputBuffer buffer : buffers) {
                String text = buffer.getOutput();
                int numLines = 0;
                int numBackslashes = 0;
                int numNonASCII = 0;
                boolean needsFinalNewline = false;
                boolean needsEscape;

                // scan for newlines and characters requiring escapes
                for (int i = 0; i < text.length(); i++) {
                    char c = text.charAt(i);
                    if (c < 32) {
                        if (c == '\n') {
                            numLines++;
                        } else if (c != '\t' && c != '\r') {
                            numNonASCII++;
                        }
                    } else if (c < 127) {
                        if (c == '\\') {
                            numBackslashes++;
                        }
                    } else {
                        numNonASCII++;
                    }
                }

                needsEscape = numBackslashes > 0 || numNonASCII > 0;

                // Check the text ends with a final newline ('\n', not line.separator)
                // Note this must match the check when reading the text back in,
                // when we also check for just '\n' and not line.separator, because
                // line.separator now, and line.separator then, might be different.
                if (!text.isEmpty() && !text.endsWith("\n")) {
                    needsFinalNewline = true;
                    numLines++;
                }

                out.write(JTR_V2_SECTSTREAM);
                out.write(buffer.getName());
                out.write(":");
                out.write('(');
                out.write(String.valueOf(numLines));
                out.write('/');
                if (needsEscape) {
                    // count one per character, plus an additional one per \ (written as "\ \") and an
                    // additional 5 per nonASCII (written as "\ u x x x x")
                    out.write(String.valueOf(text.length() + numBackslashes + 5 * numNonASCII));
                } else {
                    out.write(String.valueOf(text.length()));
                }
                out.write(')');
                if (needsEscape) {
                    out.write('*');
                }
                out.write(JTR_V2_SECTSTREAM);
                out.write(lineSeparator);

                if (needsEscape) {
                    for (int i = 0; i < text.length(); i++) {
                        char c = text.charAt(i);
                        if (32 <= c && c < 127 && c != '\\') {
                            out.write(c);
                        } else {
                            switch (c) {
                                case '\n':
                                case '\r':
                                case '\t':
                                    out.write(c);
                                    break;
                                case '\\':
                                    out.write("\\\\");
                                    break;
                                default:
                                    out.write("\\u");
                                    out.write(Character.forDigit((c >> 12) & 0xF, 16));
                                    out.write(Character.forDigit((c >> 8) & 0xF, 16));
                                    out.write(Character.forDigit((c >> 4) & 0xF, 16));
                                    out.write(Character.forDigit((c >> 0) & 0xF, 16));
                                    break;
                            }
                        }
                    }
                } else {
                    out.write(text);
                }

                if (needsFinalNewline) {
                    out.write(lineSeparator);
                }
            }

            // the default message section does not need a result line
            if (!Objects.equals(getTitle(), MSG_SECTION_NAME)) {
                out.write(JTR_V2_SECTRESULT + result.toString());
                out.write(lineSeparator);
            }

            out.write(lineSeparator);
        }

        /**
         * Reload an output block. This method is called while reloading
         * a test result and so bypasses the normal immutability checks.
         */
        synchronized void reloadOutput(String name, String data) {
            if (name.equals(MESSAGE_OUTPUT_NAME)) {
                name = MESSAGE_OUTPUT_NAME;
            }
            OutputBuffer b = new FixedOutputBuffer(name, data);
            buffers = DynamicArray.append(buffers, b);
        }

        /**
         * Reload the status. This method is called while reloading
         * a test result and so bypasses the normal immutability checks.
         */
        synchronized void reloadStatus(Status s) {
            result = s;
        }

        private void checkMutable() {
            if (!isMutable()) {
                throw new IllegalStateException("This section of the test result is now immutable.");
            }
        }

        private synchronized void makeOutputImmutable(OutputBuffer b, String name, String output) {
            for (int i = 0; i < buffers.length; i++) {
                if (buffers[i] == b) {
                    buffers[i] = new FixedOutputBuffer(name, output);
                    return;
                }
            }
        }

        private synchronized OutputBuffer findOutputBuffer(String name) {
            // search backwards
            // may help in some backward compatibility cases since the most
            // recent stream with that name will be found
            // performance of the search will still be constant
            for (int i = buffers.length - 1; i >= 0; i--) {
                if (name.equals(buffers[i].getName())) {
                    return buffers[i];
                }
            }

            return null;
        }

        private class FixedOutputBuffer implements OutputBuffer {
            private final String name;
            private final String output;

            FixedOutputBuffer(String name, String output) {
                if (name == null || output == null) {
                    throw new NullPointerException();
                }

                this.name = name;
                this.output = output;
            }

            FixedOutputBuffer(String header, BufferedReader in) throws ReloadFault {
                String nm = extractSlice(header, JTR_V2_SECTSTREAM.length(), null, ":");
                if (nm == null) {
                    throw new ReloadFault(i18n, "rslt.noOutputTitle");
                }

                if (nm.equals(MESSAGE_OUTPUT_NAME)) {
                    nm = MESSAGE_OUTPUT_NAME;
                }

                try {
                    int lines;
                    int chars;
                    boolean needsEscape;

                    try {
                        int start = JTR_V2_SECTSTREAM.length();
                        lines = Integer.parseInt(extractSlice(header, start, "(", "/"));
                        chars = Integer.parseInt(extractSlice(header, start, "/", ")"));
                        int rp = header.indexOf(')', start);
                        if (rp >= 0 && rp < header.length() - 2) {
                            needsEscape = header.charAt(rp + 1) == '*';
                        } else {
                            needsEscape = false;
                        }
                    } catch (NumberFormatException e) {
                        // fatal parsing error
                        throw new ReloadFault(i18n, "rslt.badHeaderVersion", e);
                    }

                    StringBuilder buff = new StringBuilder(chars);

                    if (needsEscape) {
                        for (int i = 0; i < chars; i++) {
                            int c = in.read();
                            if (c == -1) {
                                throw new ReloadFault(i18n, "rslt.badEOF");
                            } else if (c == '\\') {
                                c = in.read();
                                i++;
                                if (c == 'u') {
                                    c = Character.digit((char) in.read(), 16) << 12;
                                    c += Character.digit((char) in.read(), 16) << 8;
                                    c += Character.digit((char) in.read(), 16) << 4;
                                    c += Character.digit((char) in.read(), 16);
                                    i += 4;
                                }
                                // else drop through (for \\)
                            }
                            buff.append((char) c);
                        }
                    } else {
                        char[] data = new char[Math.min(4096, chars)];
                        int charsRead = 0;
                        while (charsRead < chars) {
                            int n = in.read(data, 0, Math.min(data.length, chars - charsRead));

                            // sanity check, may be truncated file
                            if (n < 0) {
                                throw new ReloadFault(i18n, "rslt.badRuntimeErr", resultsFile, Integer.toString(n));
                            }

                            buff.append(data, 0, n);
                            charsRead += n;
                        }
                    }

                    /*NEW
                    while (true) {
                        int c = in.read();
                        switch (c) {
                        case -1:
                            throw new ReloadFault(i18n, "rslt.badEOF");

                        case '\\':
                            if (needEscape) {
                                c = in.read();
                                if (c == 'u') {
                                    c =  Character.digit((char)in.read(), 16) << 12;
                                    c += Character.digit((char)in.read(), 16) <<  8;
                                    c += Character.digit((char)in.read(), 16) <<  4;
                                    c += Character.digit((char)in.read(), 16);
                                }
                                // else drop through (for \\)
                            }
                            buff.append((char)c);
                        }
                    }
                    */

                    name = nm;
                    output = buff.toString();

                    if (buff.length() > 0 && buff.charAt(buff.length() - 1) != '\n') {
                        int c = in.read();
                        if (c == '\r') {
                            c = in.read();
                        }
                        if (c != '\n') {
                            System.err.println("TR.badChars: output=" + (output.length() < 32 ? output : output.substring(0, 9) + " ... " + output.substring(output.length() - 10)));
                            System.err.println("TR.badChars: '" + (char) c + "' (" + c + ")");
                            throw new ReloadFault(i18n, "rslt.badChars", name);
                        }
                    }
                } catch (IOException e) {
                    // not enough data probably fatal parsing error
                    throw new ReloadFault(i18n, "rslt.badFile", e);
                }
            }

            @Override
            public String getName() {
                return name;
            }

            @Override
            public String getOutput() {
                return output;
            }

            @Override
            public PrintWriter getPrintWriter() {
                throw new IllegalStateException("This section is immutable");
            }
        }

        private class WritableOutputBuffer extends Writer implements OutputBuffer {
            private final String name;
            private final PrintWriter pw;
            private boolean overflowed;
            private int overflowStart;
            private /*final*/ StringBuffer output; // can't easily be final in JDK 1.1 because need to reassign to it

            WritableOutputBuffer(String name) {
                super(TestResult.this);
                if (name == null) {
                    throw new NullPointerException();
                }

                this.name = name;
                output = new StringBuffer();
                pw = new LockedWriter(this, TestResult.this);
            }

            @Override
            public String getName() {
                return name;
            }

            @Override
            public String getOutput() {
                return new String(output);
            }

            @Override
            public PrintWriter getPrintWriter() {
                return pw;
            }

            @Override
            public void write(char[] buf, int offset, int len) throws IOException {
                if (output == null) {
                    throw new IOException("stream has been closed");
                }

                int end = output.length();
                output.append(buf, offset, len);
                // want to avoid creating the string buf(offset..len)
                // since likely case is no observers
                notifyUpdatedOutput(Section.this, name, end, end, buf, offset, len);

                int maxOutputSize = maxTROutputSize > 0 ? maxTROutputSize : commonOutputSize;
                if (output.length() > maxOutputSize) {
                    int overflowEnd = maxOutputSize / 3;
                    if (overflowed) {
                        // output.delete(overflowStart, overflowEnd);
                        // JDK 1.1--start
                        String s = output.toString();
                        output = new StringBuffer(s.substring(0, overflowStart) + s.substring(output.length() - overflowEnd));
                        // JDK 1.1--end
                        notifyUpdatedOutput(Section.this, name, overflowStart, overflowEnd, "");
                    } else {
                        String OVERFLOW_MESSAGE =
                                "\n\n...\n"
                                        + "Output overflow:\n"
                                        + "JT Harness has limited the test output to the text\n"
                                        + "at the beginning and the end, so that you can see how the\n"
                                        + "test began, and how it completed.\n"
                                        + "\n"
                                        + "If you need to see more of the output from the test,\n"
                                        + "set the system property javatest.maxOutputSize to a higher\n"
                                        + "value. The current value is " + maxOutputSize
                                        + "\n...\n\n";
                        overflowStart = maxOutputSize / 3;
                        //output.replace(overflowStart, maxOutputSize*2/3, OVERFLOW_MESSAGE);
                        // JDK 1.1--start
                        String s = output.toString();
                        output = new StringBuffer(s.substring(0, overflowStart) + OVERFLOW_MESSAGE + s.substring(overflowEnd));
                        // JDK 1.1--end
                        notifyUpdatedOutput(Section.this, name, overflowStart, overflowEnd, OVERFLOW_MESSAGE);
                        overflowStart += OVERFLOW_MESSAGE.length();
                        overflowed = true;
                    }
                }
            }

            @Override
            public void flush() {
                //no-op
            }

            public void deleteAllOutput() {
                pw.flush();
                output.setLength(0);
                overflowStart = -1;
                overflowed = false;
            }

            @Override
            public void close() {
                makeOutputImmutable(this, name, new String(output));
                notifyCompletedOutput(Section.this, name);
            }
        }
    }

}
