1 /*
   2  * $Id$
   3  *
   4  * Copyright (c) 1996, 2016, Oracle and/or its affiliates. All rights reserved.
   5  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   6  *
   7  * This code is free software; you can redistribute it and/or modify it
   8  * under the terms of the GNU General Public License version 2 only, as
   9  * published by the Free Software Foundation.  Oracle designates this
  10  * particular file as subject to the "Classpath" exception as provided
  11  * by Oracle in the LICENSE file that accompanied this code.
  12  *
  13  * This code is distributed in the hope that it will be useful, but WITHOUT
  14  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  15  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  16  * version 2 for more details (a copy is included in the LICENSE file that
  17  * accompanied this code).
  18  *
  19  * You should have received a copy of the GNU General Public License version
  20  * 2 along with this work; if not, write to the Free Software Foundation,
  21  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  22  *
  23  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  24  * or visit www.oracle.com if you need additional information or have any
  25  * questions.
  26  */
  27 package com.sun.javatest;
  28 
  29 import java.io.*;
  30 import java.lang.ref.WeakReference;
  31 import java.nio.charset.StandardCharsets;
  32 import java.text.DateFormat;
  33 import java.text.ParseException;
  34 import java.text.SimpleDateFormat;
  35 import java.util.*;
  36 
  37 import com.sun.javatest.util.*;
  38 import com.sun.javatest.util.Properties;
  39 
  40 /**
  41  * The TestResult object encapsulates the results from a test.
  42  * Test results are formatted in sections of command output,
  43  * comments and sometimes "streams" of output (<tt>stdout</tt> for example).
  44  * Each of these sections is represented by a (@link TestResult.Section Section).
  45  * Instances of this class are mutable until the result of the section is
  46  * set or until the result of the test itself is set.
  47  *
  48  * Test results are stored in a structured text files.
  49  * The TestResult class serves as the API for accessing the various
  50  * components that make up the result of running a test.
  51  * The status is cached as its size is small and it is accessed often.
  52  *
  53  * This class and inner classes will throw IllegalStateExceptions if an
  54  * attempt is made to modify the any part of the object that has been
  55  * marked immutable.
  56  */
  57 
  58 public class TestResult {
  59     /**
  60      * This exception is to report problems using TestResult objects.
  61      */
  62     public static class Fault extends Exception
  63     {
  64         Fault(I18NResourceBundle i18n, String key) {
  65             super(i18n.getString(key));
  66         }
  67 
  68         Fault(I18NResourceBundle i18n, String key, Object arg) {
  69             super(i18n.getString(key, arg));
  70         }
  71 
  72         Fault(I18NResourceBundle i18n, String key, Object[] args) {
  73             super(i18n.getString(key, args));
  74         }
  75     }
  76 
  77     /**
  78      * This exception is thrown if the JTR file cannot be found.
  79      */
  80     public static class ResultFileNotFoundFault extends Fault {
  81         ResultFileNotFoundFault(I18NResourceBundle i18n, String key) {
  82             super(i18n, key);
  83         }
  84 
  85         ResultFileNotFoundFault(I18NResourceBundle i18n, String key, Object arg) {
  86             super(i18n, key, arg);
  87         }
  88 
  89         ResultFileNotFoundFault(I18NResourceBundle i18n, String key, Object[] args) {
  90             super(i18n, key, args);
  91         }
  92     }
  93 
  94     /**
  95      * This exception ay occur anytime the JTR file is being read from the filesystem.
  96      * To optimize memory usage, the contents of a TestResult object are sometimes
  97      * discarded and then loaded on demand from the JTR file.  If a fault occurs
  98      * when reading the JTR file, this fault may occur.
  99      *
 100      * @see TestResult.ResultFileNotFoundFault
 101      */
 102     public static class ReloadFault extends Fault {
 103         ReloadFault(I18NResourceBundle i18n, String key) {
 104             super(i18n, key);
 105         }
 106 
 107         ReloadFault(I18NResourceBundle i18n, String key, Object arg) {
 108             super(i18n, key, arg);
 109         }
 110 
 111         ReloadFault(I18NResourceBundle i18n, String key, Object[] args) {
 112             super(i18n, key, args);
 113         }
 114     }
 115 
 116     /**
 117      * An interface to observe activity in a TestResult as it is created.
 118      */
 119     public interface Observer {
 120         /**
 121          * A new section has been created in the test result.
 122          *
 123          * @param tr The test result in which the section was created.
 124          * @param section The section that has been created
 125          */
 126         public void createdSection(TestResult tr, Section section);
 127 
 128         /**
 129          * A section has been been completed in the test result.
 130          *
 131          * @param tr The test result containing the section.
 132          * @param section The section that has been completed.
 133          */
 134         public void completedSection(TestResult tr, Section section);
 135 
 136         /**
 137          * New output has been created in a section of the test result.
 138          *
 139          * @param tr The test result containing the output.
 140          * @param section The section in which the output has been created.
 141          * @param outputName The name of the output.
 142          */
 143         public void createdOutput(TestResult tr, Section section, String outputName);
 144 
 145         /**
 146          * Output has been completed in a section of the test result.
 147          *
 148          * @param tr The test result containing the output.
 149          * @param section The section in which the output has been completed.
 150          * @param outputName The name of the output.
 151          */
 152         public void completedOutput(TestResult tr, Section section, String outputName);
 153 
 154         /**
 155          * The output for a section has been updated.
 156          *
 157          * @param tr The test result object being modified.
 158          * @param section The section in which the output is being produced.
 159          * @param outputName The name of the output.
 160          * @param start the start offset of the text that was changed
 161          * @param end the end offset of the text that was changed
 162          * @param text the text that replaced the specified range.
 163          */
 164         public void updatedOutput(TestResult tr, Section section, String outputName, int start, int end, String text);
 165 
 166         /**
 167          * A property of the test result has been updated.
 168          *
 169          * @param tr The test result containing the property that was modified.
 170          * @param name The key for the property that was modified.
 171          * @param value The new value for the property.
 172          *
 173          */
 174         public void updatedProperty(TestResult tr, String name, String value);
 175 
 176         /**
 177          * The test has completed, and the results are now immutable.
 178          * There will be no further observer calls.
 179          * @param tr The test result that has been completed.
 180          */
 181         public void completed(TestResult tr);
 182 
 183     }
 184 
 185     /**
 186      * This "section" is the logical combination of a single action during test
 187      * execution.  It is designed to hold multiple (or none) buffers of
 188      * output from test execution, such as stdout and stderr.  In addition,
 189      * it has a "comment" field for tracking the test run itself (progress).
 190      * This output is identified by the MSG_SECTION_NAME identifier.
 191      */
 192     public class Section {
 193         /**
 194          * Query if the section is still writable or not.
 195          * @return true if the section is still writable, and false otherwise
 196          */
 197         public boolean isMutable() {
 198             synchronized (TestResult.this) {
 199                 synchronized (this) {
 200                     return (TestResult.this.isMutable() &&
 201                             this.result == inProgress);
 202                 }
 203             }
 204         }
 205 
 206         /**
 207          * Find out what the result of the execution of this section was.
 208          * @return the result of the execution of this section
 209          * @see #setStatus
 210          */
 211         public Status getStatus() {
 212             return result;
 213         }
 214 
 215         /**
 216          * Set the result of this section.  This action makes this section
 217          * immutable.
 218          *
 219          * @param result The status to set as the result of this section of the test
 220          * @see #getStatus
 221          */
 222         public void setStatus(Status result) {
 223             synchronized (TestResult.this) {
 224                 synchronized (this) {
 225                     checkMutable();
 226                     for (int i = 0; i < buffers.length; i++) {
 227                         OutputBuffer b = buffers[i];
 228                         if (b instanceof WritableOutputBuffer) {
 229                             WritableOutputBuffer wb = (WritableOutputBuffer)b;
 230                             wb.getPrintWriter().close();
 231                         }
 232                     }
 233                     if (env == null)
 234                         env = emptyStringArray;
 235                     this.result = result;
 236                     if (env == null)
 237                         env = emptyStringArray;
 238                     notifyCompletedSection(this);
 239                 }
 240             }
 241         }
 242 
 243         /**
 244          * Get the title of this section, specified when the section
 245          * was created.
 246          * @return the title of this section
 247          */
 248         public String getTitle() {
 249             return title;
 250         }
 251 
 252         /**
 253          * Get the appropriate to writer to access the default message field.
 254          * @return a Writer to access the default message field
 255          */
 256         public PrintWriter getMessageWriter() {
 257             synchronized (TestResult.this) {
 258                 synchronized (this) {
 259                     checkMutable();
 260                     // if it is mutable, it must have a message stream,
 261                     // which will be the first entry
 262                     return buffers[0].getPrintWriter();
 263                 }
 264             }
 265         }
 266 
 267         /**
 268          * Find out how many output buffers this section has inside it.
 269          *
 270          * @return The number of output buffers in use (&gt;=0).
 271          */
 272         public synchronized int getOutputCount() {
 273             return buffers.length;
 274         }
 275 
 276         /**
 277          * Add a new output buffer to the section; get PrintWriter access to it.
 278          *
 279          * @param name The symbolic name that will identify this new stream.
 280          * @return A PrintWriter that gives access to the new stream.
 281          */
 282         public PrintWriter createOutput(String name) {
 283             if (name == null)
 284                 throw new NullPointerException();
 285 
 286             synchronized (TestResult.this) {
 287                 synchronized (this) {
 288                     checkMutable();
 289 
 290                     OutputBuffer b = new WritableOutputBuffer(name);
 291                     buffers = DynamicArray.append(buffers, b);
 292 
 293                     notifyCreatedOutput(this, name);
 294 
 295                     return b.getPrintWriter();
 296                 }
 297             }
 298         }
 299 
 300         /**
 301          * Get the content that was written to a specified output stream.
 302          * @param name the name of the stream in question
 303          * @return All the data that was written to the specified output,
 304          *         or null if nothing has been written.
 305          */
 306         public String getOutput(String name) {
 307             if (name == null)
 308                 throw new NullPointerException();
 309 
 310             synchronized (TestResult.this) {
 311                 synchronized (this) {
 312                     OutputBuffer b = findOutputBuffer(name);
 313                     return (b == null ? null : b.getOutput());
 314                 }
 315             }
 316         }
 317 
 318         /**
 319          * Find out the symbolic names of all the streams in this section.  You
 320          * can use getOutputCount to discover the number of items in
 321          * this enumeration (not a thread safe activity in the strictest
 322          * sense of course).
 323          *
 324          * @return A list of strings which are the symbolic names of the streams in this section.
 325          * @see #getOutputCount
 326          */
 327         public synchronized String[] getOutputNames() {
 328             String[] names = new String[buffers.length];
 329 
 330             for (int i = 0; i < buffers.length; i++) {
 331                 names[i] = buffers[i].getName();
 332                 if (names[i] == null)
 333                     throw new IllegalStateException("BUFFER IS BROKEN");
 334             }
 335 
 336             return names;
 337         }
 338 
 339         /**
 340          * Removes any data added to the named output up to this point, resetting
 341          * it to an empty state.
 342          * @param name The output name to erase the content of.
 343          * @since 4.2.1
 344          */
 345         public synchronized void deleteOutputData(String name) {
 346             if (name == null)
 347                 throw new NullPointerException();
 348 
 349             synchronized (TestResult.this) {
 350                 synchronized (this) {
 351                     OutputBuffer b = findOutputBuffer(name);
 352                     if (b != null && b instanceof WritableOutputBuffer)
 353                         ((WritableOutputBuffer)b).deleteAllOutput();
 354                 }
 355             }
 356         }
 357 
 358         // ---------- PACKAGE PRIVATE ----------
 359 
 360         public Section(String title) {
 361             if (title == null)
 362                 throw new NullPointerException();
 363             if (title.indexOf(' ') != -1)
 364                 throw new IllegalArgumentException("space invalid in section title");
 365 
 366             this.title = title;
 367             result = inProgress;
 368         }
 369 
 370         /**
 371          * Could be used to reconstruct the section from a stream.
 372          * Reads from the source until it finds a section header.  This is a JTR
 373          * version 2 method, don't use it for version 1 files.  The object
 374          * immediately immutable upon return from this constructor.
 375          *
 376          * @throws ReloadFault Probably an error while parsing the input stream.
 377          */
 378         Section(BufferedReader in) throws IOException, ReloadFault {
 379             String line = in.readLine();
 380             // find top of section and process it
 381             while (line != null) {
 382                 if (line.startsWith(JTR_V2_SECTION)) {
 383                     title = extractSlice(line, 0, ":", null);
 384                     break;
 385                 }
 386                 else
 387                     // don't know what this line is, may be empty
 388                     line = in.readLine();
 389             }
 390 
 391             if (title == null)
 392                 throw new ReloadFault(i18n, "rslt.noSectionTitle");
 393 
 394             if (title.equals(MSG_SECTION_NAME)) {
 395                 // use standard internal copy of string
 396                 title = MSG_SECTION_NAME;
 397             }
 398 
 399             while ((line = in.readLine()).startsWith(JTR_V2_SECTSTREAM)) {
 400                 OutputBuffer b = new FixedOutputBuffer(line, in);
 401                 buffers = DynamicArray.append(buffers, b);
 402             }
 403 
 404             // if not in the message section, line should have the section result
 405             if (title != MSG_SECTION_NAME) {
 406                 if (line != null) {
 407                     if (line.startsWith(JTR_V2_SECTRESULT))
 408                         result = Status.parse(line.substring(JTR_V2_SECTRESULT.length()));
 409                     else
 410                         throw new ReloadFault(i18n, "rslt.badLine", line);
 411                 }
 412                 if (result == null)
 413                     // no test result
 414                     throw new ReloadFault(i18n, "rslt.noSectionResult");
 415             }
 416         }
 417 
 418         void save(Writer out) throws IOException {
 419             out.write(JTR_V2_SECTION + getTitle());
 420             out.write(lineSeparator);
 421 
 422             for (int index = 0; index < buffers.length; index++) {
 423                 String text = buffers[index].getOutput();
 424                 int numLines = 0;
 425                 int numBackslashes = 0;
 426                 int numNonASCII = 0;
 427                 boolean needsFinalNewline = false;
 428                 boolean needsEscape;
 429 
 430                 // scan for newlines and characters requiring escapes
 431                 for (int i = 0; i < text.length(); i++) {
 432                     char c = text.charAt(i);
 433                     if (c < 32) {
 434                         if (c == '\n')
 435                             numLines++;
 436                         else if (c != '\t' && c != '\r')
 437                             numNonASCII++;
 438                     }
 439                     else if (c < 127) {
 440                         if (c == '\\')
 441                             numBackslashes++;
 442                     }
 443                     else
 444                         numNonASCII++;
 445                 }
 446 
 447                 needsEscape = (numBackslashes > 0 || numNonASCII > 0);
 448 
 449                 // Check the text ends with a final newline ('\n', not line.separator)
 450                 // Note this must match the check when reading the text back in,
 451                 // when we also check for just '\n' and not line.separator, because
 452                 // line.separator now, and line.separator then, might be different.
 453                 if (text.length() != 0 && !text.endsWith("\n")) {
 454                     needsFinalNewline = true;
 455                     numLines++;
 456                 }
 457 
 458                 out.write(JTR_V2_SECTSTREAM);
 459                 out.write(buffers[index].getName());
 460                 out.write(":");
 461                 out.write('(');
 462                 out.write(String.valueOf(numLines));
 463                 out.write('/');
 464                 if (needsEscape) {
 465                     // count one per character, plus an additional one per \ (written as "\ \") and an
 466                     // additional 5 per nonASCII (written as "\ u x x x x")
 467                     out.write(String.valueOf(text.length() + numBackslashes + 5*numNonASCII));
 468                 }
 469                 else
 470                     out.write(String.valueOf(text.length()));
 471                 out.write(')');
 472                 if (needsEscape)
 473                     out.write('*');
 474                 out.write(JTR_V2_SECTSTREAM);
 475                 out.write(lineSeparator);
 476 
 477                 if (needsEscape) {
 478                     for (int i = 0; i < text.length(); i++) {
 479                         char c = text.charAt(i);
 480                         if (32 <= c && c < 127 && c != '\\')
 481                             out.write(c);
 482                         else {
 483                             switch (c) {
 484                             case '\n': case '\r': case '\t':
 485                                 out.write(c);
 486                                 break;
 487                             case '\\':
 488                                 out.write("\\\\");
 489                                 break;
 490                             default:
 491                                 out.write("\\u");
 492                                 out.write(Character.forDigit((c >> 12) & 0xF, 16));
 493                                 out.write(Character.forDigit((c >>  8) & 0xF, 16));
 494                                 out.write(Character.forDigit((c >>  4) & 0xF, 16));
 495                                 out.write(Character.forDigit((c >>  0) & 0xF, 16));
 496                                 break;
 497                             }
 498                         }
 499                     }
 500                 }
 501                 else
 502                     out.write(text);
 503 
 504                 if (needsFinalNewline)
 505                     out.write(lineSeparator);
 506             }
 507 
 508             // the default message section does not need a result line
 509             if (getTitle() != MSG_SECTION_NAME) {
 510                 out.write(JTR_V2_SECTRESULT + result.toString());
 511                 out.write(lineSeparator);
 512             }
 513 
 514             out.write(lineSeparator);
 515         }
 516 
 517         /**
 518          * Reload an output block. This method is called while reloading
 519          * a test result and so bypasses the normal immutability checks.
 520          */
 521         synchronized void reloadOutput(String name, String data) {
 522             if (name.equals(MESSAGE_OUTPUT_NAME))
 523                 name = MESSAGE_OUTPUT_NAME;
 524             OutputBuffer b = new FixedOutputBuffer(name, data);
 525             buffers = DynamicArray.append(buffers, b);
 526         }
 527 
 528         /**
 529          * Reload the status. This method is called while reloading
 530          * a test result and so bypasses the normal immutability checks.
 531          */
 532         synchronized void reloadStatus(Status s) {
 533             result = s;
 534         }
 535 
 536         // ---------- PRIVATE ----------
 537 
 538         private void checkMutable() {
 539             if (!isMutable())
 540                 throw new IllegalStateException("This section of the test result is now immutable.");
 541         }
 542 
 543         private synchronized void makeOutputImmutable(OutputBuffer b, String name, String output) {
 544             for (int i = 0; i < buffers.length; i++) {
 545                 if (buffers[i] == b) {
 546                     buffers[i] = new FixedOutputBuffer(name, output);
 547                     return;
 548                 }
 549             }
 550         }
 551 
 552         private synchronized OutputBuffer findOutputBuffer(String name) {
 553             // search backwards
 554             // may help in some backward compatibility cases since the most
 555             // recent stream with that name will be found
 556             // performance of the search will still be constant
 557             for (int i = buffers.length-1; i >= 0 ; i--) {
 558                 if (name.equals(buffers[i].getName()))
 559                     return buffers[i];
 560             }
 561 
 562             return null;
 563         }
 564 
 565         private OutputBuffer[] buffers = new OutputBuffer[0];
 566         private String title;
 567         private Status result;
 568 
 569         private class FixedOutputBuffer implements OutputBuffer {
 570             FixedOutputBuffer(String name, String output) {
 571                 if (name == null || output == null)
 572                     throw new NullPointerException();
 573 
 574                 this.name = name;
 575                 this.output = output;
 576             }
 577 
 578             public String getName() {
 579                 return name;
 580             }
 581 
 582             public String getOutput() {
 583                 return output;
 584             }
 585 
 586             public PrintWriter getPrintWriter() {
 587                 throw new IllegalStateException("This section is immutable");
 588             }
 589 
 590             FixedOutputBuffer(String header, BufferedReader in) throws ReloadFault {
 591                 String nm = extractSlice(header, JTR_V2_SECTSTREAM.length(), null, ":");
 592                 if (nm == null)
 593                     throw new ReloadFault(i18n, "rslt.noOutputTitle");
 594 
 595                 if (nm.equals(MESSAGE_OUTPUT_NAME ))
 596                     nm = MESSAGE_OUTPUT_NAME;
 597 
 598                 try {
 599                     int lines;
 600                     int chars;
 601                     boolean needsEscape;
 602 
 603                     try {
 604                         int start = JTR_V2_SECTSTREAM.length();
 605                         lines = Integer.parseInt(extractSlice(header, start, "(", "/"));
 606                         chars = Integer.parseInt(extractSlice(header, start, "/", ")"));
 607                         int rp = header.indexOf(")", start);
 608                         if (rp >= 0 && rp < header.length() - 2)
 609                             needsEscape = (header.charAt(rp + 1) == '*');
 610                         else
 611                             needsEscape = false;
 612                     }
 613                     catch (NumberFormatException e) {
 614                         // fatal parsing error
 615                         throw new ReloadFault(i18n, "rslt.badHeaderVersion", e);
 616                     }
 617 
 618                     StringBuffer buff = new StringBuffer(chars);
 619 
 620                     if (needsEscape) {
 621                         for (int i = 0; i < chars; i++) {
 622                             int c = in.read();
 623                             if (c == -1)
 624                                 throw new ReloadFault(i18n, "rslt.badEOF");
 625                             else if (c == '\\') {
 626                                 c = in.read();
 627                                 i++;
 628                                 if (c == 'u') {
 629                                     c =  Character.digit((char)in.read(), 16) << 12;
 630                                     c += Character.digit((char)in.read(), 16) <<  8;
 631                                     c += Character.digit((char)in.read(), 16) <<  4;
 632                                     c += Character.digit((char)in.read(), 16);
 633                                     i += 4;
 634                                 }
 635                                 // else drop through (for \\)
 636                             }
 637                             buff.append((char)c);
 638                         }
 639                     }
 640                     else {
 641                         char[] data = new char[Math.min(4096, chars)];
 642                         int charsRead = 0;
 643                         while (charsRead < chars) {
 644                             int n = in.read(data, 0, Math.min(data.length, chars-charsRead));
 645 
 646                             // sanity check, may be truncated file
 647                             if (n < 0) {
 648                                 throw new ReloadFault(i18n, "rslt.badRuntimeErr", new Object[]
 649                                     {resultsFile, Integer.toString(n)});
 650                             }
 651 
 652                             buff.append(data, 0, n);
 653                             charsRead += n;
 654                         }
 655                     }
 656 
 657                     /*NEW
 658                     while (true) {
 659                         int c = in.read();
 660                         switch (c) {
 661                         case -1:
 662                             throw new ReloadFault(i18n, "rslt.badEOF");
 663 
 664                         case '\\':
 665                             if (needEscape) {
 666                                 c = in.read();
 667                                 if (c == 'u') {
 668                                     c =  Character.digit((char)in.read(), 16) << 12;
 669                                     c += Character.digit((char)in.read(), 16) <<  8;
 670                                     c += Character.digit((char)in.read(), 16) <<  4;
 671                                     c += Character.digit((char)in.read(), 16);
 672                                 }
 673                                 // else drop through (for \\)
 674                             }
 675                             buff.append((char)c);
 676                         }
 677                     }
 678                     */
 679 
 680                     name = nm;
 681                     output = buff.toString();
 682 
 683                     if (buff.length() > 0 && buff.charAt(buff.length() - 1) != '\n') {
 684                         int c = in.read();
 685                         if (c == '\r')
 686                             c = in.read();
 687                         if (c != '\n') {
 688                             System.err.println("TR.badChars: output=" + (output.length() < 32 ? output : output.substring(0, 9) + " ... " + output.substring(output.length() - 10) ));
 689                             System.err.println("TR.badChars: '" + ((char)c) + "' (" + c + ")");
 690                             throw new ReloadFault(i18n, "rslt.badChars", name);
 691                         }
 692                     }
 693                 }
 694                 catch (IOException e) {
 695                     // not enough data probably fatal parsing error
 696                     throw new ReloadFault(i18n, "rslt.badFile", e);
 697                 }
 698             }
 699 
 700             private final String name;
 701             private final String output;
 702         }
 703 
 704         private class WritableOutputBuffer extends Writer implements OutputBuffer {
 705             WritableOutputBuffer(String name) {
 706                 super(TestResult.this);
 707                 if (name == null)
 708                     throw new NullPointerException();
 709 
 710                 this.name = name;
 711                 output = new StringBuffer();
 712                 pw = new LockedWriter(this, TestResult.this);
 713             }
 714 
 715             public String getName() {
 716                 return name;
 717             }
 718 
 719             public String getOutput() {
 720                 return new String(output);
 721             }
 722 
 723             public PrintWriter getPrintWriter() {
 724                 return pw;
 725             }
 726 
 727             public void write(char[] buf, int offset, int len) throws IOException {
 728                 if (output == null)
 729                     throw new IOException("stream has been closed");
 730 
 731                 int end = output.length();
 732                 output.append(buf, offset, len);
 733                 // want to avoid creating the string buf(offset..len)
 734                 // since likely case is no observers
 735                 notifyUpdatedOutput(Section.this, name, end, end, buf, offset, len);
 736 
 737                 int maxOutputSize = maxTROutputSize > 0 ? maxTROutputSize : commonOutputSize;
 738                 if (output.length() > maxOutputSize) {
 739                     int overflowEnd = maxOutputSize/3;
 740                     if (overflowed) {
 741                         // output.delete(overflowStart, overflowEnd);
 742                         // JDK 1.1--start
 743                         String s = output.toString();
 744                         output = new StringBuffer(s.substring(0, overflowStart) + s.substring(output.length()-overflowEnd));
 745                         // JDK 1.1--end
 746                         notifyUpdatedOutput(Section.this, name, overflowStart, overflowEnd, "");
 747                     }
 748                     else {
 749                         String OVERFLOW_MESSAGE =
 750                             "\n\n...\n"
 751                             + "Output overflow:\n"
 752                             + "JT Harness has limited the test output to the text to that\n"
 753                             + "at the beginning and the end, so that you can see how the\n"
 754                             + "test began, and how it completed.\n"
 755                             + "\n"
 756                             + "If you need to see more of the output from the test,\n"
 757                             + "set the system property javatest.maxOutputSize to a higher\n"
 758                             + "value. The current value is " + maxOutputSize
 759                             + "\n...\n\n";
 760                         overflowStart = maxOutputSize/3;
 761                         //output.replace(overflowStart, maxOutputSize*2/3, OVERFLOW_MESSAGE);
 762                         // JDK 1.1--start
 763                         String s = output.toString();
 764                         output = new StringBuffer(s.substring(0, overflowStart) + OVERFLOW_MESSAGE + s.substring(overflowEnd));
 765                         // JDK 1.1--end
 766                         notifyUpdatedOutput(Section.this, name, overflowStart, overflowEnd, OVERFLOW_MESSAGE);
 767                         overflowStart += OVERFLOW_MESSAGE.length();
 768                         overflowed = true;
 769                     }
 770                 }
 771             }
 772 
 773             public void flush() {
 774                 //no-op
 775             }
 776 
 777             public void deleteAllOutput() {
 778                 pw.flush();
 779                 output.setLength(0);
 780                 overflowStart = -1;
 781                 overflowed = false;
 782             }
 783 
 784             public void close() {
 785                 makeOutputImmutable(this, name, new String(output));
 786                 notifyCompletedOutput(Section.this, name);
 787             }
 788 
 789             private boolean overflowed;
 790             private int overflowStart;
 791             private final String name;
 792             private /*final*/ StringBuffer output; // can't easily be final in JDK 1.1 because need to reassign to it
 793             private final PrintWriter pw;
 794         }
 795     }
 796 
 797     private class LockedWriter extends PrintWriter {
 798         public LockedWriter(Writer out, Object theLock) {
 799             super(out);
 800             lock = theLock;
 801         }
 802     }
 803 
 804     // Conceptually, this belongs in Section, but that is not legal Java.
 805     // (It is accepted in  1.1.x; rejected by 1.2)
 806     private interface OutputBuffer {
 807         String getName();
 808         String getOutput();
 809         PrintWriter getPrintWriter();
 810     }
 811 
 812 
 813 
 814 
 815     // ------------------------- PUBLIC CONSTRUCTORS -------------------------
 816     /**
 817      * Construct a test result object that will be built as the test runs.
 818      * The status string will be "running..." rather than "not run".
 819      *
 820      * @param td The test description to base this new object on.  Cannot be
 821      *        null.
 822      */
 823     public TestResult(TestDescription td) {
 824         desc = td;
 825         execStatus = inProgress;
 826         testURL = desc.getRootRelativeURL();
 827 
 828         createSection(MSG_SECTION_NAME);
 829 
 830         props = emptyStringArray;  // null implies it was discarded, not empty
 831     }
 832 
 833 
 834     /**
 835      * Reconstruct the results of a previously run test.
 836      *
 837      * @param workDir Work directory in which the tests were run
 838      * @param td      Description of the test that was run
 839      * @throws TestResult.Fault if there is a problem recreating the results
 840      *                  from the appropriate file in the work directory
 841      */
 842     public TestResult(TestDescription td, WorkDirectory workDir)
 843                       throws Fault {
 844         desc = td;
 845         testURL = desc.getRootRelativeURL();
 846         execStatus = inProgress;
 847 
 848         reloadFromWorkDir(workDir);
 849     }
 850 
 851     /**
 852      * Reconstruct the results of a previously run test.
 853      *
 854      * @param file File that the results have been stored into.
 855      * @throws     TestResult.ReloadFault if there is a problem recreating the results
 856      *                  from the given file
 857      * @throws     TestResult.ResultFileNotFoundFault if there is a problem locating
 858      *                  the given file
 859      */
 860     public TestResult(File file)
 861         throws ResultFileNotFoundFault, ReloadFault
 862     {
 863         resultsFile = file;
 864         reload();
 865 
 866         testURL = desc.getRootRelativeURL();
 867 
 868         execStatus = Status.parse(PropertyArray.get(props, EXEC_STATUS));
 869     }
 870 
 871     /**
 872      * Reconstruct the results of a previously run test.
 873      *
 874      * @param workDir The work directory where previous results for the guven
 875      *        test can be found.
 876      * @param workRelativePath The path to the JTR to reload, relative to the
 877      *        workdir.
 878      * @throws TestResult.Fault if there is a problem recreating the results
 879      *            from the given file
 880      */
 881     public TestResult(WorkDirectory workDir, String workRelativePath) throws Fault {
 882         //resultsFile = workDir.getFile(workRelativePath.replace('/', File.separatorChar));
 883         resultsFile = workDir.getFile(workRelativePath);
 884         reload();
 885 
 886         testURL = desc.getRootRelativeURL();
 887         execStatus = Status.parse(PropertyArray.get(props, EXEC_STATUS));
 888     }
 889 
 890     /**
 891      * Create a temporary test result for which can be handed around
 892      * in situations where a reasonable test result can't be created.
 893      *
 894      * @param td     Description of the test
 895      * @param s      Status to associate with running the test... presumed
 896      *               to be of the Status.FAILED type.
 897      */
 898     public TestResult(TestDescription td, Status s) {
 899         desc = td;
 900         testURL = desc.getRootRelativeURL();
 901         resultsFile = null;
 902         execStatus = s;
 903         props = emptyStringArray;
 904     }
 905 
 906     /**
 907      * Create a placeholder TestResult for a test that has not yet been run.
 908      *
 909      * @param td     The test description for the test
 910      * @return       A test result that indicates that the test has not yet been run
 911      */
 912     public static TestResult notRun(TestDescription td) {
 913         return new TestResult(td, notRunStatus);
 914     }
 915 
 916     //------------------------ MODIFIER METHODS ------------------------------
 917 
 918     /**
 919      * Create a new section inside this test result.
 920      *
 921      * @param name The symbolic name for this new section.
 922      * @return The new section that was created.
 923      */
 924     public synchronized TestResult.Section createSection(String name) {
 925         if (!isMutable()) {
 926             throw new IllegalStateException(
 927                         "This TestResult is no longer mutable!");
 928         }
 929 
 930         Section section = new Section(name);
 931         sections = DynamicArray.append(sections, section);
 932         notifyCreatedSection(section);
 933         // avoid creating output (which will cause observer messages)
 934         // before the createdSection has been notified
 935         section.createOutput(TestResult.MESSAGE_OUTPUT_NAME);
 936 
 937         return section;
 938     }
 939 
 940 
 941     /**
 942      * Set the environment used by this test. When the test is run,
 943      * those entries in the environment that are referenced are noted;
 944      * those entries will be recorded here in the test result object.
 945      * @param environment the test environment used by this test.
 946      * @see #getEnvironment
 947      */
 948     public synchronized void setEnvironment(TestEnvironment environment) {
 949         if (!isMutable()) {
 950             throw new IllegalStateException(
 951                         "This TestResult is no longer mutable!");
 952         }
 953         for (Iterator<TestEnvironment.Element> i = environment.elementsUsed().iterator(); i.hasNext(); ) {
 954             TestEnvironment.Element elem = i.next();
 955             // this is stunningly inefficient and should be fixed
 956             env = PropertyArray.put(env, elem.getKey(), elem.getValue());
 957         }
 958     }
 959 
 960     /**
 961      * Set the result of this test.  This action makes this object immutable.
 962      * If a result comparison is needed, it will be done in here.
 963      * @param stat A status object representing the outcome of the test
 964      * @see #getStatus
 965      */
 966     public synchronized void setStatus(Status stat) {
 967         if (!isMutable()) {
 968             throw new IllegalStateException(
 969                         "This TestResult is no longer mutable!");
 970         }
 971 
 972         if (stat == null) {
 973             throw new IllegalArgumentException(
 974                         "TestResult status cannot be set to null!");
 975         }
 976 
 977         // close out message section
 978         sections[0].setStatus(null);
 979 
 980         execStatus = stat;
 981 
 982         if (execStatus == inProgress)
 983             execStatus = interrupted;
 984 
 985         // verify integrity of status in all sections
 986         for (int i = 0; i < sections.length; i++) {
 987             if (sections[i].isMutable()) {
 988                 sections[i].setStatus(incomplete);
 989             }
 990         }
 991 
 992         props = PropertyArray.put(props, SECTIONS,
 993                                   StringArray.join(getSectionTitles()));
 994         props = PropertyArray.put(props, EXEC_STATUS,
 995                                   execStatus.toString());
 996 
 997         // end time now required
 998         // mainly for writing in the TRC for the Last Run Filter
 999         if (PropertyArray.get(props, END) == null) {
1000             props = PropertyArray.put(props, END, formatDate(new Date()));
1001         }
1002 
1003         // this object is now immutable
1004         notifyCompleted();
1005     }
1006 
1007     /**
1008      * Add a new property value to this TestResult.
1009      *
1010      * @param name The name of the property to be updated.
1011      * @param value The new value of the specified property.
1012      */
1013     public synchronized void putProperty(String name, String value) {
1014         // check mutability
1015         if (!isMutable()) {
1016             throw new IllegalStateException(
1017                 "Cannot put property, the TestResult is no longer mutable!");
1018         }
1019 
1020         props = PropertyArray.put(props, name, value);
1021         notifyUpdatedProperty(name, value);
1022     }
1023 
1024     /**
1025      * Sets the maximum output size for the current TestResult.
1026      * The value will be used instead of the value specified
1027      * by the system property javatest.maxOutputSize.
1028      * @param size the maximum number of characters.
1029      */
1030     public synchronized void setMaxOutputSize(int size){
1031         if (!isMutable()) {
1032             throw new IllegalStateException(
1033                     "This TestResult is no longer mutable!");
1034         }
1035         maxTROutputSize = size;
1036     }
1037 
1038     /**
1039      * Reconstruct the results of a previously run test.
1040      *
1041      * @param workDir Work directory in which the tests were run
1042      * @throws TestResult.Fault if an error occurs while reloading the results
1043      */
1044     public void reloadFromWorkDir(WorkDirectory workDir) throws Fault {
1045         // check mutability
1046         if (!isMutable()) {
1047             throw new IllegalStateException(
1048                 "Cannot reload results, the TestResult is no longer mutable!");
1049         }
1050 
1051 
1052         try {
1053             resultsFile = workDir.getFile(getWorkRelativePath());
1054             props = null;
1055             sections = null;
1056             execStatus = null;
1057 
1058             reload(new InputStreamReader(new FileInputStream(resultsFile), StandardCharsets.UTF_8));
1059 
1060             // this next line is dubious since the execStatus should have
1061             // been set during the reload
1062             execStatus = Status.parse(PropertyArray.get(props, EXEC_STATUS));
1063         }
1064         catch (FileNotFoundException e) {
1065             props = emptyStringArray;
1066             env = emptyStringArray;
1067             sections = emptySectionArray;
1068             execStatus = Status.notRun("no test result file found");
1069         }
1070         catch (IOException e) {
1071             props = emptyStringArray;
1072             env = emptyStringArray;
1073             sections = emptySectionArray;
1074             execStatus = Status.error("error opening result file: " + e);
1075             throw new Fault(i18n, "rslt.badFile", e.toString());
1076         }
1077         catch (Fault f) {
1078             props = emptyStringArray;
1079             env = emptyStringArray;
1080             sections = emptySectionArray;
1081             execStatus = Status.error(f.getMessage());
1082             throw f;
1083         }
1084 
1085     }
1086 
1087     //----------ACCESS FUNCTIONS (MISC)-----------------------------------------
1088 
1089 
1090     /**
1091      * A code indicating that no checksum was found in a .jtr file.
1092      * @see #getChecksumState
1093      */
1094     public static final int NO_CHECKSUM = 0;
1095 
1096     /**
1097      * A code indicating that an invalid checksum was found in a .jtr file.
1098      * @see #getChecksumState
1099      */
1100     public static final int BAD_CHECKSUM = 1;
1101 
1102     /**
1103      * A code indicating that a good checksum was found in a .jtr file.
1104      * @see #getChecksumState
1105      */
1106     public static final int GOOD_CHECKSUM = 2;
1107 
1108     /**
1109      * The number of different checksum states (none, good, bad).
1110      */
1111     public static final int NUM_CHECKSUM_STATES = 3;
1112 
1113 
1114     /**
1115      * Get info about the checksum in this object.
1116      * @return a value indicating the validity or otherwise of the checksum
1117      * found while reading this result object.
1118      * @see #NO_CHECKSUM
1119      * @see #BAD_CHECKSUM
1120      * @see #GOOD_CHECKSUM
1121      */
1122     public byte getChecksumState() {
1123         return checksumState;
1124     }
1125 
1126     /**
1127      * A way to write comments about the test execution into the results.
1128      *
1129      * @return If this is null, then the object is in a state in which it
1130      *         does not accept new messages.
1131      */
1132     public PrintWriter getTestCommentWriter() {
1133         return sections[0].getMessageWriter();
1134     }
1135 
1136     /**
1137      * Get the test name, as given by the test URL defined by
1138      * TestDescription.getRootRelativeURL().  This method <em>always</em>
1139      * returns a useful string, representing the test name.
1140      *
1141      * @return the name of the test for which this is the result object
1142      * @see TestDescription#getRootRelativeURL
1143      */
1144     public String getTestName() {
1145         return testURL;
1146     }
1147 
1148     /**
1149      * Check whether this test result can be reloaded from a file.
1150      * This method does not validate the contents of the file.
1151      * @return true if the result file for this object can be read
1152      */
1153     public boolean isReloadable() {
1154         return (resultsFile != null && resultsFile.canRead());
1155     }
1156 
1157     /**
1158      * Check whether this object has been "shrunk" to reduce its
1159      * memory footprint. If it has, some or all of the data will have
1160      * to be reloaded.  This method is somewhat
1161      * orthogonal to <code>isReloadable()</code> and should not be used as a
1162      * substitute.
1163      *
1164      * @return True if this object is currently incomplete, false otherwise.
1165      * @see #isReloadable
1166      */
1167     public boolean isShrunk() {
1168         if (!isMutable() &&
1169             (desc == null ||
1170              props == null ||
1171              env == null ||
1172              (sections == null && execStatus != inProgress)))
1173             return true;
1174         else
1175             return false;
1176     }
1177 
1178     /**
1179      * Get the description of the test from which this result was created.
1180      * Depending on how the test result was created, this information may
1181      * not be immediately available, and may be recreated from the test
1182      * result file.
1183      *
1184      * @return the test description for this test result object
1185      * @throws TestResult.Fault if there is a problem recreating the description
1186      * from the results file.
1187      */
1188     public synchronized TestDescription getDescription()
1189                 throws Fault {
1190         if (desc == null) {
1191             // reconstitute description (probably from file)
1192             reload();
1193         }
1194         return desc;
1195     }
1196 
1197     /*
1198      * Get the title of this test. This info originally comes from the test
1199      * description, but is saved in the .jtr file as well.
1200      *
1201      * @deprecated Please query the test description for info.
1202     public String getTitle() {
1203         // hmm slight copout; would like to make sure never null in the first place
1204         String title = desc.getParameter("title");
1205         if (title == null)
1206             title = td.getRootRelativeURL();
1207     }
1208      */
1209 
1210     /**
1211      * Get the path name for the results file for this test, relative to the
1212      * work directory.  The internal separator is '/'.
1213      * @return the path name for the results file for this test,
1214      * relative to the work directory
1215      */
1216     public String getWorkRelativePath() {
1217         return getWorkRelativePath(testURL);
1218     }
1219 
1220     /**
1221      * Get the name, if any, for the result file for this object.
1222      * The path information contains platform specific path separators.
1223      * @return the name, if any, for the result file for this object
1224      */
1225     public File getFile() {
1226         return resultsFile;
1227     }
1228 
1229     public void resetFile() {
1230         resultsFile = null;
1231     }
1232 
1233     /**
1234      * Get the path name for the results file for a test, relative to the
1235      * work directory.  The internal separator is '/'.
1236      * @param td the test description for the test in question
1237      * @return the path name for the results file for a test, relative to the
1238      * work directory
1239      */
1240     public static String getWorkRelativePath(TestDescription td) {
1241         String baseURL = td.getRootRelativePath();
1242 
1243         // add in uniquifying id if
1244         String id = td.getParameter("id");
1245         return getWorkRelativePath(baseURL, id);
1246     }
1247 
1248     /**
1249      * Get the path name for the results file for a test, relative to the
1250      * work directory.  The internal separator is '/'.
1251      *
1252      * @param testURL May not be null;
1253      * @return The work relative path of the JTR for this test.  Null if the
1254      *         given URL is null.
1255      */
1256     public static String getWorkRelativePath(String testURL) {
1257         int pound = testURL.lastIndexOf("#");
1258         if (pound == -1)        // no test id
1259             return getWorkRelativePath(testURL, null);
1260         else
1261             return getWorkRelativePath(testURL.substring(0, pound),
1262                                        testURL.substring(pound + 1));
1263     }
1264 
1265     /**
1266      * Get the path name for the results file for a test, relative to the
1267      * work directory.  The internal separator is '/'.
1268      *
1269      * @param baseURL May not be null;
1270      * @param testId The test identifier that goes with the URL.  This may be null.
1271      * @return The work relative path of the JTR for this test.  Null if the
1272      *         given URL is null.
1273      */
1274     public static String getWorkRelativePath(String baseURL, String testId) {
1275         StringBuffer sb = new StringBuffer(baseURL);
1276 
1277         // strip off extension
1278     stripExtn:
1279         for (int i = sb.length() - 1; i >= 0; i--) {
1280             switch (sb.charAt(i)) {
1281             case '.':
1282                 sb.setLength(i);
1283                 break stripExtn;
1284             case '/':
1285                 break stripExtn;
1286             }
1287         }
1288 
1289         // add in uniquifying id if
1290         if (testId != null) {
1291             sb.append('_');
1292             sb.append(testId);
1293         }
1294 
1295         sb.append(EXTN);
1296 
1297         return sb.toString();
1298     }
1299 
1300     /**
1301      * Get the keys of the properties that this object has stored.
1302      * @return the keys of the properties that this object has stored
1303      */
1304     public synchronized Enumeration<String> getPropertyNames() {
1305         return PropertyArray.enumerate(props);
1306     }
1307 
1308     /**
1309      * Get the value of a property of this test result.
1310      *
1311      * @param name The name of the property to be retrieved.
1312      * @return The value corresponding to the property name, null if not
1313      *          found.
1314      * @throws TestResult.Fault if there is a problem
1315      *          recreating data from the results file.
1316      */
1317     public synchronized String getProperty(String name)
1318             throws Fault {
1319         if (props == null) {
1320             // reconstitute properties
1321             // this may result in a Fault, which is okay
1322             reload();
1323         }
1324 
1325         return PropertyArray.get(props, name);
1326     }
1327 
1328     /**
1329      * Get a copy of the environment that this object has stored.
1330      * @return a copy of the environment that this object has stored
1331      * @throws TestResult.Fault if there is a problem
1332      *          recreating data from the results file.
1333      * @see #setEnvironment
1334      */
1335     public synchronized Map<String, String> getEnvironment() throws Fault {
1336         if (env == null) {
1337             // reconstitute environment
1338             // this may result in a Fault, which is okay
1339             reload();
1340         }
1341         return PropertyArray.getProperties(env);
1342     }
1343 
1344     /**
1345      * Get the parent node in the test result table that
1346      * contains this test result object.
1347      * @return the parent node in the test result table that
1348      * contains this test result object.
1349      */
1350     public TestResultTable.TreeNode getParent() {
1351         return parent;
1352     }
1353 
1354 
1355     /**
1356      * Set the parent node in the test result table that
1357      * contains this test result object.
1358      * @param p the parent node in the test result table that
1359      * contains this test result object.
1360      * @see #getParent
1361      */
1362     void setParent(TestResultTable.TreeNode p) {
1363         parent = p;
1364     }
1365 
1366     //----------ACCESS FUNCTIONS (TEST STATUS)----------------------------------
1367 
1368     /**
1369      * Determine if the test result object is still mutable.
1370      * Test results are only mutable while they are being created, up to
1371      * the point that the final status is set.
1372      * @return true if the test result object is still mutable,
1373      * and false otherwise
1374      */
1375     public synchronized boolean isMutable() {
1376         // must be mutable during reload (internal operation)
1377         // mutable as long as possible, to allow max time for writing log messages
1378         return (execStatus == inProgress);
1379     }
1380 
1381 
1382     /**
1383      * Get the status for this test.
1384      * @return the status for this test
1385      * @see #setStatus
1386      */
1387     public synchronized Status getStatus() {
1388         return execStatus;
1389     }
1390 
1391     //----------ACCESS METHODS (TEST OUTPUT)----------------------------------
1392 
1393     /**
1394      * Find out how many sections this test result contains.
1395      *
1396      * @return The number of sections in this result.
1397      */
1398     public synchronized int getSectionCount() {
1399         if (sections != null) {
1400             return sections.length;
1401         }
1402         else if (PropertyArray.get(props, SECTIONS) != null) {
1403             return parseSectionCount(PropertyArray.get(props, SECTIONS));
1404         }
1405         else {
1406             // hum, test props are never discarded, so we have no sections
1407             return 0;
1408         }
1409     }
1410 
1411     /**
1412      * Get the section specified by index.
1413      * Remember that index 0 is the default message section.
1414      *
1415      * @param index The index of the section to be retrieved.
1416      * @return The requested section.  Will be null if the section does not exist.
1417      * @throws TestResult.ReloadFault Will occur if an error is encountered when reloading
1418      *         JTR data.  This may be the result of a corrupt or missing JTR file.
1419      * @see #MSG_SECTION_NAME
1420      */
1421     public synchronized Section getSection(int index) throws ReloadFault {
1422         Section target;
1423 
1424         if (sections == null && execStatus != inProgress) {
1425             // try to reload from file
1426             try {
1427                 reload();
1428             }
1429             catch (ReloadFault f) {
1430                 throw f;
1431             }
1432             catch (Fault f) {
1433                 throw new ReloadFault(i18n, "rslt.badFile",  f.getMessage());
1434             }
1435         }
1436 
1437         if (index >= sections.length) {
1438             target = null;
1439         }
1440         else {
1441             target = sections[index];
1442         }
1443 
1444         return target;
1445     }
1446 
1447     /**
1448      * Get the titles of all sections in this test result.
1449      * A null result probably indicates that there are no sections.  This is
1450      * improbable since most test result object automatically have one section.
1451      *
1452      * @return The titles, one at a time in the array.  Null if the titles
1453      *          do not exist or could not be determined.
1454      */
1455     public synchronized String[] getSectionTitles() {
1456         if (props == null) {
1457             try {
1458                 reload();
1459             }
1460             catch (Fault f) {
1461                 // should this maybe be a JavaTestError?
1462                 return null;
1463             }
1464         }
1465 
1466         // look for what we need from easiest to hardest source
1467         String names = PropertyArray.get(props, SECTIONS);
1468 
1469         if (names != null) {
1470             // it is cached
1471             return StringArray.split(names);
1472         }
1473         else if (sections != null) {
1474             // TR is not immutable yet, probably
1475             int numSections = getSectionCount();
1476             String[] data = new String[numSections];
1477 
1478             for (int i = 0; i < numSections; i++) {
1479                 data[i] = sections[i].getTitle();
1480             }
1481 
1482             return data;
1483         }
1484         else {
1485             // hum, bad.  No sections exist and this data isn't cached
1486             // the test probably has not run
1487             return null;
1488         }
1489     }
1490 
1491     /**
1492      * Check if this file is or appears to be a result (.jtr) file,
1493      * according to its filename extension.
1494      * @param f the file to be checked
1495      * @return true if this file is or appears to be a result (.jtr) file.
1496      */
1497     public static boolean isResultFile(File f) {
1498         String p = f.getPath();
1499         return (p.endsWith(EXTN));
1500     }
1501 
1502     /**
1503      * Writes the TestResult into a version 2 jtr file.
1504      *
1505      * @param workDir The work directory in which to write the results
1506      * @param backupPolicy a policy object defining what to do if a file
1507      * already exists with the same name as that which is about to be written.
1508      * @throws IllegalStateException This will occur if you attempt to write a result
1509      *         which is still mutable.
1510      * @throws IOException Occurs when the output file cannot be created or written to.
1511      *         Under this condition, this object will change it status to an error.
1512      */
1513     public synchronized void writeResults(WorkDirectory workDir, BackupPolicy backupPolicy)
1514                 throws IOException
1515     {
1516         if (isMutable())
1517             throw new IllegalStateException("This TestResult is still mutable - set the status!");
1518 
1519         // could attempt a reload() I suppose
1520         if (props == null)
1521             props = emptyStringArray;
1522 
1523         String wrp = getWorkRelativePath(desc).replace('/', File.separatorChar);
1524         resultsFile = workDir.getFile(wrp);
1525 
1526         File resultsDir = resultsFile.getParentFile();
1527         resultsDir.mkdirs(); // ensure directory created for .jtr file
1528 
1529         File tempFile = createTempFile(workDir, backupPolicy);
1530         try {
1531             writeResults(tempFile, backupPolicy);
1532         }
1533         finally {
1534             if (tempFile.exists())
1535                 tempFile.delete();
1536         }
1537     }
1538 
1539     /**
1540      * Create a temporary file to which the results can be written, before being renamed
1541      * to its real name.
1542      */
1543     // don't use File.createTempFile because of issues with the internal locking there
1544     private File createTempFile(WorkDirectory workDir, BackupPolicy backupPolicy)
1545         throws IOException
1546     {
1547         final int MAX_TRIES = 100; // absurdly big limit, but a limit nonetheless
1548         for (int i = 0; i < MAX_TRIES; i++) {
1549             File tempFile = new File(resultsFile.getPath() + "." + i + ".tmp");
1550             if (tempFile.createNewFile())
1551                 return tempFile;
1552         }
1553         throw new IOException("could not create temp file for " + resultsFile + ": too many tries");
1554     }
1555 
1556     /**
1557      * Write the results to a temporary file, and when done, rename it to resultsFile
1558      */
1559     private void writeResults(File tempFile, BackupPolicy backupPolicy)
1560         throws IOException
1561     {
1562         Writer out;
1563         try {
1564             out = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(tempFile), StandardCharsets.UTF_8));
1565         }
1566         catch (IOException e) {
1567             execStatus = Status.error("Problem writing result file for test: " + getTestName());
1568             resultsFile = null; // file not successfully written after all
1569             throw e;
1570         }
1571 
1572         try {
1573             // redundant, is done in setResult
1574             // needed though if setResult isn't being called
1575             props = PropertyArray.put(props, EXEC_STATUS, execStatus.toString());
1576 
1577             // file header
1578             out.write(JTR_V2_HEADER);
1579             out.write(lineSeparator);
1580 
1581             // date and time
1582             out.write("#" + (new Date()).toString());
1583             out.write(lineSeparator);
1584 
1585             // checksum header and data
1586             //out.write(JTR_V2_CHECKSUM);
1587             //out.write(Long.toHexString(computeChecksum()));
1588             //out.write(lineSeparator);
1589  /*
1590             if (debug) {  // debugging code
1591                 out.write("# debug: test desc checksum: ");
1592                 out.write(Long.toHexString(computeChecksum(desc)));
1593                 out.write(lineSeparator);
1594 
1595                 for (Iterator iter = desc.getParameterKeys(); iter.hasNext(); ) {
1596                     // don't rely on enumeration in a particular order
1597                     // so simply add the checksum products together
1598                     String KEY = (String) (iter.next());
1599                     out.write("# debug: test desc checksum key " + KEY + ": ");
1600                     out.write(Long.toHexString(computeChecksum(KEY) * computeChecksum(desc.getParameter(KEY))));
1601                     out.write(lineSeparator);
1602                 }
1603 
1604                 out.write("# debug: test env checksum: ");
1605                 if (env == null)
1606                     out.write("null");
1607                 else
1608                     out.write(Long.toHexString(computeChecksum(env)));
1609                 out.write(lineSeparator);
1610 
1611                 out.write("# debug: test props checksum: ");
1612                 out.write(Long.toHexString(computeChecksum(props)));
1613                 out.write(lineSeparator);
1614 
1615                 out.write("# debug: test sections checksum: ");
1616                 out.write(Long.toHexString(computeChecksum(sections)));
1617                 out.write(lineSeparator);
1618 
1619                 for (int I = 0; I < sections.length; I++) {
1620                     out.write("# debug: test section[" + I + "] checksum: ");
1621                     out.write(Long.toHexString(computeChecksum(sections[I])));
1622                     out.write(lineSeparator);
1623 
1624                     String[] NAMES = sections[I].getOutputNames();
1625                     for (int J = 0; J < NAMES.length; J++) {
1626                         out.write("# debug: test section[" + I + "] name=" + NAMES[J] + " checksum: ");
1627                         out.write(Long.toHexString(computeChecksum(NAMES[J])));
1628                         out.write(lineSeparator);
1629 
1630                         out.write("# debug: test section[" + I + "] name=" + NAMES[J] + " output checksum: ");
1631                         out.write(Long.toHexString(computeChecksum(sections[I].getOutput(NAMES[J]))));
1632                         out.write(lineSeparator);
1633                     }
1634                 }
1635             }*/
1636 
1637             // description header and data
1638             out.write(JTR_V2_TESTDESC);
1639             out.write(lineSeparator);
1640 
1641             Map<String, String> tdProps = new HashMap<>();
1642             desc.save(tdProps);
1643             PropertyArray.save(PropertyArray.getArray(tdProps), out);
1644             out.write(lineSeparator);
1645 
1646             // test environment header and data
1647             if (env != null) {
1648                 out.write(JTR_V2_ENVIRONMENT);
1649                 out.write(lineSeparator);
1650                 PropertyArray.save(env, out);
1651                 out.write(lineSeparator);
1652             }
1653 
1654             // test result props header and data
1655             out.write(JTR_V2_RESPROPS);
1656             out.write(lineSeparator);
1657             PropertyArray.save(props, out);
1658             out.write(lineSeparator);
1659 
1660             // get sections into memory
1661             // I hope the out stream is not the same as the resultFile!
1662             if (sections == null) {
1663                 throw new JavaTestError("Cannot write test result - it contains no sections.");
1664             }
1665 
1666             for (int i = 0; i < sections.length; i++) {
1667                 sections[i].save(out);
1668             }
1669 
1670             out.write(lineSeparator);
1671             out.write(JTR_V2_TSTRESULT);
1672             out.write(execStatus.toString());
1673             out.write(lineSeparator);
1674             out.close();
1675         }   // try
1676         catch (IOException e) {
1677             // This exception could be raised when trying to create the directory
1678             // for the test results; opening the results file, or closing it.
1679             execStatus = Status.error("Write to temp. JTR file failed (old JTR intact): " +
1680                                         tempFile.getPath());
1681             resultsFile = null; // file not successfully written after all
1682             throw e;
1683         }   // catch
1684 
1685         try {
1686             backupPolicy.backupAndRename(tempFile, resultsFile);
1687 
1688             // now that it has been successfully written out, make the object
1689             // a candidate for shrinking
1690             addToShrinkList();
1691         }   // try
1692         catch (IOException e) {
1693             // This exception could be raised when trying to create the directory
1694             // for the test results; opening the results file, or closing it.
1695             execStatus = Status.error("Problem writing result file: " +
1696                                         resultsFile.getPath());
1697             resultsFile = null; // file not successfully written after all
1698             throw e;
1699         }   // catch
1700     }
1701 
1702     // -----observer methods ---------------------------------------------------
1703     /**
1704      * Add an observer to watch this test result for changes.
1705      * @param obs the observer to be added
1706      */
1707     public synchronized void addObserver(Observer obs) {
1708         if (isMutable()) {
1709             Observer[] observers = observersTable.get(this);
1710 
1711             if (observers == null) observers = new Observer[0];
1712 
1713             observers = DynamicArray.append(observers, obs);
1714             observersTable.put(this, observers);
1715         }
1716     }
1717 
1718     /**
1719      * Remove an observer that was previously added.
1720      * @param obs the observer to be removed
1721      */
1722     public synchronized void removeObserver(Observer obs) {
1723         Observer[] observers = observersTable.get(this);
1724         if (observers == null)
1725             return;
1726 
1727         observers = DynamicArray.remove(observers, obs);
1728         if (observers == null)
1729             observersTable.remove(this);
1730         else
1731             observersTable.put(this, observers);
1732     }
1733 
1734     /**
1735      * Gets the time when the test was completed, or at least the time
1736      * when it's final status was set.  Be aware that if the information is
1737      * not available in memory, it will be loaded from disk.
1738      *
1739      * @return Time when this test acquired its final status setting.
1740      * @see #setStatus
1741      * @see java.util.Date
1742      */
1743     public long getEndTime() {
1744         if (endTime < 0) {
1745             try {
1746                 String datestr = PropertyArray.get(props, END);
1747 
1748                 if (datestr == null) {
1749                     // this may be more expensive because it can cause a
1750                     // reload from disk
1751                     try {
1752                         datestr = getProperty(END);
1753                     }
1754                     catch (Fault f) {
1755                     }
1756                 }
1757 
1758                 if (datestr != null) {
1759                     Date date = parseDate(datestr);
1760                     endTime = date.getTime();
1761                 }
1762                 else {
1763                     // info not available
1764                 }
1765             }
1766             catch (ParseException e) {
1767             }
1768         }
1769 
1770         return endTime;
1771     }
1772 
1773     /**
1774      * Parse the date format used for timestamps, such as the start/stop timestamp.
1775      * @param s The string containing the date to be restored.
1776      * @see #formatDate
1777      */
1778     public static synchronized Date parseDate(String s) throws ParseException {
1779         return dateFormat.parse(s);
1780     }
1781 
1782     /**
1783      * Format the date format used for timestamps, such as the start/stop timestamp.
1784      * @param d The date object to be formatted into a string.
1785      * @see #parseDate
1786      */
1787     public static synchronized String formatDate(Date d) {
1788         return dateFormat.format(d);
1789     }
1790 
1791     // ----- PACKAGE METHODS ---------------------------------------------------
1792 
1793     /**
1794      * Read a single minimal TestResult from a .jts stream.
1795      * The stream is not closed.
1796      * @deprecated JTS files are no longer supported
1797     TestResult(WorkDirectory workDir, DataInputStream in) throws IOException {
1798         workRelativePath = in.readUTF();
1799 
1800         // ** temp. fix ** XXX
1801         // make sure the path is in URL form with forward slashes
1802         // in the future all paths should already be of this form (TestDescription)
1803         int index = workRelativePath.indexOf('/');
1804         if (index == -1) workRelativePath = workRelativePath.replace('\\', '/');
1805 
1806         resultsFile = workDir.getFile(workRelativePath.replace('/', File.separatorChar));
1807         title = in.readUTF();
1808         int esc = in.readByte();
1809         String esr = in.readUTF();
1810         execStatus = new Status(esc, esr);
1811         boolean defIsExec = in.readBoolean();
1812         if (!defIsExec) {
1813             // have to read these, per protocol
1814             int dsc = in.readByte();
1815             String dsr = in.readUTF();
1816             //ignore dsc, dsr; they used to go in defStatus
1817         }
1818     }
1819      */
1820 
1821     /**
1822      * Read a single minimal TestResult which is capable of reloading itself.
1823      * None of the parameters may be null.
1824      *
1825      * @param url The full URL of this test, including test id.
1826      * @param workDir The work directory location, platform specfic path.
1827      * @param status The status that will be found in the JTR.
1828      * @throws JavaTestError Will be thrown if any params are null.
1829      */
1830     TestResult(String url, WorkDirectory workDir, Status status) {
1831         if (url == null)
1832             throw new JavaTestError(i18n, "rslt.badTestUrl");
1833 
1834         if (workDir == null)
1835             throw new JavaTestError(i18n, "rslt.badWorkdir");
1836 
1837         if (status == null)
1838             throw new JavaTestError(i18n, "rslt.badStatus");
1839 
1840         testURL = url;
1841         resultsFile = workDir.getFile(getWorkRelativePath());
1842         execStatus = status;
1843     }
1844 
1845     /**
1846      * Read a single minimal TestResult which is capable of reloading itself.
1847      * None of the parameters may be null.
1848      *
1849      * @param url The full URL of this test, including test id.
1850      * @param workDir The work directory location, platform specific path.
1851      * @param status The status that will be found in the JTR.
1852      * @param endTime The time when that test finished execution.
1853      * @throws JavaTestError Will be thrown if any params are null.
1854      * @see #getEndTime()
1855      */
1856     TestResult(String url, WorkDirectory workDir, Status status, long endTime) {
1857         if (url == null)
1858             throw new JavaTestError(i18n, "rslt.badTestUrl");
1859 
1860         if (workDir == null)
1861             throw new JavaTestError(i18n, "rslt.badWorkdir");
1862 
1863         if (status == null)
1864             throw new JavaTestError(i18n, "rslt.badStatus");
1865 
1866         testURL = url;
1867         resultsFile = workDir.getFile(getWorkRelativePath());
1868         execStatus = status;
1869         this.endTime = endTime;
1870     }
1871 
1872     void shareStatus(Map<String, Status>[] tables) {
1873         execStatus = shareStatus(tables, execStatus);
1874     }
1875 
1876     /*
1877      * @deprecated JTS files no longer supported
1878     void writeSummary(DataOutputStream out) throws IOException {
1879         out.writeUTF(workRelativePath);
1880         out.writeUTF(title);
1881         out.writeByte(execStatus.getType());
1882         out.writeUTF(execStatus.getReason());
1883         out.writeBoolean(true); // defStatus == execStatus
1884     }
1885     */
1886 
1887     String[] getTags() {
1888         // Script or someone else could possibly do this w/the observer
1889         if (sections == null) {
1890             return null;
1891         }
1892 
1893         Vector<String> tagV = new Vector<>(sections.length * 2);
1894 
1895         for (int i = 0; i < sections.length; i++) {
1896             String[] names = sections[i].getOutputNames();
1897 
1898             for (int j = 0; j < names.length; j++) {
1899                 tagV.addElement(names[j]);
1900             }   // inner for
1901         } // outer for
1902 
1903         String[] tagA = new String[tagV.size()];
1904         tagV.copyInto(tagA);
1905 
1906         return tagA;
1907     }
1908 
1909     /**
1910      * Insert a test description into this test results.
1911      * This will only work if the test description is currently not available.
1912      * The name in the test description must match the name of this test.
1913      * @param td The new test description, a null value will have no effect.
1914      * @see #isShrunk()
1915      * @throws IllegalStateException If the state of this object fobiu
1916      */
1917     void setTestDescription(TestDescription td) {
1918         if (td == null)
1919             return;
1920 
1921         String name = td.getRootRelativeURL();
1922         if (!testURL.equals(name))
1923             throw new IllegalStateException();
1924 
1925         if (desc != null) {             // compare if possible
1926             if (!desc.equals(td)) {     // test descriptions are not the same
1927                 // accept new TD, reset this TR
1928                 // reset status to a special one
1929                 execStatus = tdMismatch;
1930                 desc = td;
1931 
1932                 props = emptyStringArray;
1933                 resultsFile = null;
1934                 env = emptyStringArray;
1935                 sections = emptySectionArray;
1936 
1937                 if (isMutable())
1938                     createSection(MSG_SECTION_NAME);
1939             }
1940             else {
1941                 // TDs are equal, no action, drop thru and return
1942             }
1943         }
1944         else {
1945             desc = td;
1946         }
1947     }
1948 
1949     // ----- PRIVATE METHODS ---------------------------------------------------
1950 
1951     /**
1952      * @deprecated Use the Section API to accomplish your task.
1953      */
1954     private static Reader getLastRefOutput(TestResult tr) {
1955         try {
1956             Section lastBlk = tr.getSection(tr.getSectionCount() - 1);
1957             return new StringReader(lastBlk.getOutput("ref"));
1958         }
1959         catch (ReloadFault f) {
1960             // not the best, but this method is deprecated and hopefully never
1961             // called
1962             return null;
1963         }
1964     }
1965 
1966     private long computeChecksum() {
1967         long cs = 0;
1968         cs = cs * 37 + computeChecksum(desc);
1969         // in JT2.1.1a, environment was not included in checksum,
1970         // so allow that for backward compatibility
1971         String jtv = PropertyArray.get(props, VERSION);
1972         if (env != null) {
1973             if (jtv == null || !jtv.equals("JT_2.1.1a"))
1974                 cs = cs * 37 + computeChecksum(env);
1975         }
1976         cs = cs * 37 + computeChecksum(props);
1977         if (sections != null)
1978             cs = cs * 37 + computeChecksum(sections);
1979         cs = cs * 37 + execStatus.getType() + computeChecksum(execStatus.getReason());
1980         return Math.abs(cs);  // ensure +ve, to avoid sign issues!
1981     }
1982 
1983     private static long computeChecksum(TestDescription td) {
1984         long cs = 0;
1985         for (Iterator<String> i = td.getParameterKeys(); i.hasNext(); ) {
1986             // don't rely on enumeration in a particular order
1987             // so simply add the checksum products together
1988             String key = (i.next());
1989             cs += computeChecksum(key) * computeChecksum(td.getParameter(key));
1990         }
1991         return cs;
1992     }
1993 
1994     private static long computeChecksum(Section[] sections) {
1995         long cs = sections.length;
1996         for (int i = 0; i < sections.length; i++) {
1997             cs = cs * 37 + computeChecksum(sections[i]);
1998         }
1999         return cs;
2000     }
2001 
2002     private static long computeChecksum(Section s) {
2003         long cs = computeChecksum(s.getTitle());
2004         String[] names = s.getOutputNames();
2005         for (int i = 0; i <names.length; i++) {
2006             cs = cs * 37 + computeChecksum(names[i]);
2007             cs = cs * 37 + computeChecksum(s.getOutput(names[i]));
2008         }
2009         return cs;
2010     }
2011 
2012     private static long computeChecksum(String[] strings) {
2013         long cs = strings.length;
2014         for (int i = 0; i < strings.length; i++) {
2015             cs = cs * 37 + computeChecksum(strings[i]);
2016         }
2017         return cs;
2018     }
2019 
2020     private static long computeChecksum(String s) {
2021         long cs = 0;
2022         for (int i = 0; i < s.length(); i++) {
2023             char c = s.charAt(i);
2024             //if (!Character.isISOControl(c) || c == '\n')
2025             cs = cs * 37 + c;
2026         }
2027         return cs;
2028     }
2029 
2030     /**
2031      * @throws ResultFileNotFoundFault May be thrown if the JTR file cannot be found.
2032      * @throws ReloadFault Generally describes any error which is encountered while
2033      *            reading or processing the input file.
2034      */
2035     private synchronized void reload()
2036         throws ResultFileNotFoundFault, ReloadFault
2037     {
2038         if (resultsFile == null)
2039             throw new ReloadFault(i18n, "rslt.noResultFile");
2040 
2041         if (isMutable())
2042             throw new IllegalStateException("Cannot do a reload of this object.");
2043 
2044         try {
2045             reload(new BufferedReader(new InputStreamReader(new FileInputStream(resultsFile), StandardCharsets.UTF_8)));
2046 
2047             // Well, we have successfully reloaded it, so the object is now taking
2048             // up a big footprint again ... put it back on the list to be shrunk again
2049             addToShrinkList();
2050         }
2051         catch (FileNotFoundException e) {
2052             throw new ResultFileNotFoundFault(i18n, "rslt.fileNotFound", resultsFile);
2053         }
2054         catch (IOException e) {
2055             throw new ReloadFault(i18n, "rslt.badFile", e);
2056         }
2057     }
2058 
2059     /**
2060      * @throws ReloadFault Generally describes any error which is encountered while
2061      *            reading or processing the input file.  This may indicate
2062      *            an empty file or incorrectly formatted file.
2063      */
2064     private void reload(Reader r)
2065         throws ReloadFault, IOException
2066     {
2067         try {
2068             BufferedReader br = new BufferedReader(r);
2069             String line = br.readLine();
2070 
2071             // determine JTR version
2072             if (line == null) {
2073                 throw new ReloadFault(i18n, "rslt.empty", resultsFile);
2074             }
2075             if (line.equals(JTR_V2_HEADER)) {
2076                 reloadVersion2(br);
2077             }
2078             else if (line.equals(JTR_V1_HEADER)) {
2079                 reloadVersion1(br);
2080             }
2081             else
2082                 throw new ReloadFault(i18n, "rslt.badHeader", resultsFile);
2083         }
2084         catch (RuntimeException e) {
2085             throw new ReloadFault(i18n, "rslt.badRuntimeErr",
2086                     new String[] {resultsFile.getPath(), e.getLocalizedMessage()});
2087         }
2088         finally {
2089             r.close();
2090         }
2091     }
2092 
2093     private void reloadVersion1(BufferedReader in)
2094         throws ReloadFault, IOException
2095     {
2096         // grab property info
2097         StringBuffer buff = new StringBuffer();
2098         String line = in.readLine();
2099         while (!(line == null) && !(line.length() == 0)) {
2100             buff.append(line);
2101             buff.append(lineSeparator);
2102             line = in.readLine();
2103         }
2104 
2105         // store if needed
2106         Properties pairs = new Properties();
2107         if (props == null || desc == null) {
2108             StringReader sr = new StringReader(buff.toString());
2109             buff = null;
2110             line = null;
2111 
2112             pairs = new Properties();
2113             pairs.load(sr);
2114         }
2115 
2116         if (props == null) {
2117             // reload test result properties
2118             props = PropertyArray.getArray(pairs);
2119         }
2120 
2121         pairs = null;
2122 
2123         if (desc == null) {
2124             File path = new File(PropertyArray.get(props, "testsuite"));
2125             if (!path.isDirectory())
2126                 path = new File(path.getParent());
2127             File file = new File(PropertyArray.get(props, "file"));
2128 
2129             uniquifyStrings(props);
2130 
2131             desc = new TestDescription(path, file,
2132                                        PropertyArray.getProperties(props));
2133         }
2134 
2135         buff = new StringBuffer();
2136         line = in.readLine();
2137         while (!(line == null)) {
2138             if (line.startsWith("command: ")) {
2139                 // a section
2140                 Section blk = processOldSection(line, in);
2141 
2142                 if (blk != null) {
2143                     sections = DynamicArray.append(sections, blk);
2144                 }
2145             }
2146             else if (line.startsWith(JTR_V1_TSTRESULT)) {
2147                 // test result
2148                 if (line == null) {
2149                     // couldn't get the status text for some reason
2150                 }
2151                 else {
2152                     line = extractSlice(line, JTR_V1_TSTRESULT.length(), " ", null);
2153                     execStatus = Status.parse(line);
2154                 }   // inner else
2155 
2156                 break;
2157             }
2158             else {
2159                 // message text
2160                 buff.append(line);
2161                 buff.append(lineSeparator);
2162             }   // else
2163 
2164             line = in.readLine();
2165         }   // while
2166 
2167         // create the test message section and put first in the array
2168         Section blk = new Section(MSG_SECTION_NAME);
2169         blk.reloadOutput(MESSAGE_OUTPUT_NAME, buff.toString());
2170         Section[] tempBlks = new Section[sections.length+1];
2171         tempBlks[0] = blk;
2172         System.arraycopy(sections, 0, tempBlks, 1, sections.length);
2173         sections = tempBlks;
2174     }
2175 
2176     private Section processOldSection(String line1, BufferedReader in)
2177         throws ReloadFault, IOException
2178     {
2179         StringBuffer sb = new StringBuffer();         // message stream
2180         Section section = null;
2181         String line = line1;
2182         while (!(line == null)) {
2183             if (line.startsWith("----------")) {
2184                 String streamName = null;
2185                 String sectionName = null;
2186                 StringBuffer buff = new StringBuffer();
2187                 int lines = 0;
2188                 int chars = 0;
2189                 try {
2190                     streamName = extractSlice(line, 10, null, ":");
2191                     sectionName = extractSlice(line, 10, ":", "(");
2192                     lines = Integer.parseInt(extractSlice(line, 10, "(", "/"));
2193                     chars = Integer.parseInt(extractSlice(line, 10, "/", ")"));
2194 
2195                     for (int count = 0; count < lines; count++) {
2196                         buff.append(in.readLine());
2197                     }
2198                 }
2199                 catch (NumberFormatException e) {
2200                     // confused!
2201                     throw new ReloadFault(i18n, "rslt.badFile", e);
2202                 }
2203 
2204                 if (section == null)
2205                     section = new Section(sectionName);
2206 
2207                 section.reloadOutput(streamName, buff.toString());
2208             }
2209             else if (line.startsWith(JTR_V1_SECTRESULT)) {
2210                 // set result
2211                 if (section == null)
2212                     section = new Section("");
2213 
2214                 // get the Status text
2215                 line = extractSlice(line, JTR_V1_SECTRESULT.length(), " ", null);
2216 
2217                 if (line == null)
2218                     // couldn't get the status text for some reason
2219                     throw new ReloadFault(i18n, "rslt.noSectionResult");
2220                 else
2221                     section.reloadStatus(Status.parse(line));
2222 
2223                 break;
2224             }
2225             else {
2226                 // just a plain message
2227                 sb.append(line);
2228                 sb.append(lineSeparator);
2229             }
2230 
2231             line = in.readLine();
2232         }
2233 
2234         if (section != null)
2235             section.reloadOutput(MESSAGE_OUTPUT_NAME, sb.toString());
2236 
2237         return section;
2238     }
2239 
2240     private void reloadVersion2(BufferedReader in)
2241         throws ReloadFault, IOException
2242     {
2243         //String checksumText = null;
2244         String line;
2245 
2246         // look for optional checksum and then test description,
2247         // skipping comments
2248         while ((line = in.readLine()) != null) {
2249             if (line.equals(JTR_V2_TESTDESC))
2250                 break;
2251             //else if (line.startsWith(JTR_V2_CHECKSUM)) {
2252                 //checksumText = line.substring(JTR_V2_CHECKSUM.length());
2253             //}
2254             else if (!line.startsWith("#"))
2255                 throw new ReloadFault(i18n, "rslt.badLine", line);
2256         }
2257 
2258         // this probably won't work with a normal Properties object
2259         String[] tdProps = PropertyArray.load(in);
2260 
2261         if (desc == null) {
2262             uniquifyStrings(tdProps);
2263             desc = TestDescription.load(tdProps);
2264         }
2265         tdProps = null;                // dump it
2266 
2267         // XXX compare to TD
2268 
2269         // remove comment lines and look for test env props
2270         while ((line = in.readLine()) != null) {
2271             if (line.startsWith(JTR_V2_RESPROPS))
2272                 break;
2273             else if (line.startsWith(JTR_V2_ENVIRONMENT)) {
2274                 env = PropertyArray.load(in);
2275                 uniquifyStrings(env);
2276             }
2277             else if (!line.startsWith("#"))
2278                 throw new ReloadFault(i18n, "rslt.badLine", line);
2279         }
2280 
2281         if (env == null)
2282             env = new String[] {};
2283 
2284         if (line == null) {
2285             throw new ReloadFault(i18n, "rslt.badFormat");
2286         }
2287 
2288         String[] trProps = PropertyArray.load(in);
2289 
2290         if (props == null) {
2291             // restore the properties of this result
2292             uniquifyStrings(trProps);
2293             props = trProps;
2294         }
2295 
2296         trProps = null;             // dump it
2297 
2298         // read the sections
2299         int sectionCount = parseSectionCount(PropertyArray.get(props, SECTIONS));
2300         sections = new Section[sectionCount];
2301         for (int i = 0; i < getSectionCount(); i++) {
2302             sections[i] = new Section(in);
2303         }
2304 
2305         // get the final test status
2306         while ((line = in.readLine()) != null) {
2307             if (line.startsWith(JTR_V2_TSTRESULT)) {
2308                 execStatus = Status.parse(line.substring(JTR_V2_TSTRESULT.length()));
2309                 break;
2310             }
2311         }
2312 
2313         if (execStatus == null)
2314             execStatus = Status.error("NO STATUS RECORDED IN FILE");
2315 
2316         // checksum support removed
2317         checksumState = NO_CHECKSUM;
2318     }
2319 
2320     /**
2321      * This method tolerates null.  It expects a list of section names - basically
2322      * a space separated list and returns the number of items there.
2323      * @param s The section name list string to parse and count.  May be null.
2324      * @return Number of sections listed in the string.  Will be zero if the
2325      *     input was null.
2326      */
2327     int parseSectionCount(String s) {
2328         if (s == null || s.length() == 0) {
2329             return 0;
2330         }
2331 
2332         return StringArray.split(s).length;
2333     }
2334 
2335     void uniquifyStrings(String[] data) {
2336         for (int i = 0; i < data.length; i++)
2337             // don't do this for large strings
2338             if (data[i] != null && data[i].length() < 30)
2339                 data[i] = data[i].intern();
2340     }
2341 
2342     /**
2343      * Extract a substring specified by a start and end pattern (string).
2344      * The start and end strings must be single chars.
2345      * @param s String to do this operation on
2346      * @param where Position in the string to start at
2347      * @param start Beginning pattern for the slice, exclusive.
2348      * @param end Ending pattern for the slice, exclusive.  Null means
2349      *            to-end-of-string.
2350      * @return The requested substring or null if error.
2351      */
2352     String extractSlice(String s, int where, String start, String end) {
2353         int startInd;
2354         int endInd;
2355 
2356         if (start == null)
2357             startInd = where;
2358         else {
2359             int i = s.indexOf(start, where);
2360             if (i < 0)
2361                 return null;
2362             startInd = i + start.length();
2363         }
2364 
2365         if (end == null)
2366             endInd = s.length();
2367         else {
2368             endInd = s.indexOf(end, startInd);
2369             if (endInd == -1)
2370                 return null;
2371         }
2372 
2373         try {
2374             return s.substring(startInd, endInd);
2375         }
2376         catch (StringIndexOutOfBoundsException e) {
2377             return null;
2378         }
2379     }
2380 
2381 
2382     private static boolean compare(Reader left, Reader right)
2383                 throws Fault {
2384         try {
2385             try {
2386                 for (;;) {
2387                     int l = left.read(), r = right.read();
2388                     if (l != r) {
2389                         return false; // different content found
2390                     }
2391                     if (l == -1)
2392                         return true;
2393                 }
2394             }
2395             finally {
2396                 left.close();
2397                 right.close();
2398             }
2399         }
2400         catch (IOException e) {
2401             throw new Fault(i18n, "rslt.badCompare", e);
2402         }
2403     }
2404 
2405     private static Status shareStatus(Map<String, Status>[] tables, Status s) {
2406         int type = s.getType();
2407         String reason = s.getReason();
2408         Status result = tables[type].get(reason);
2409         if (result == null) {
2410             tables[type].put(reason, s);
2411             result = s;
2412         }
2413 
2414         return result;
2415     }
2416 
2417     // ------------------------ OBSERVER MAINTENANCE -------------------------
2418 
2419     /**
2420      * Notify observers that a new section has been created.
2421      *
2422      * @param section The section that was created.
2423      */
2424     private synchronized void notifyCreatedSection(Section section) {
2425         Observer[] observers = observersTable.get(this);
2426         if (observers != null)
2427             for (int i = 0; i < observers.length; i++)
2428                 observers[i].createdSection(this, section);
2429     }
2430 
2431     /**
2432      * Notify observers that a section has been completed.
2433      *
2434      * @param section The section that was completed.
2435      */
2436     private synchronized void notifyCompletedSection(Section section) {
2437         Observer[] observers = observersTable.get(this);
2438         if (observers != null)
2439             for (int i = 0; i < observers.length; i++)
2440                 observers[i].completedSection(this, section);
2441     }
2442 
2443     /**
2444      * Notify observers that new output is being created.
2445      *
2446      * @param section The section that was created.
2447      * @param outputName The name of the output.
2448      */
2449     private synchronized void notifyCreatedOutput(Section section, String outputName) {
2450         Observer[] observers = observersTable.get(this);
2451         if (observers != null)
2452             for (int i = 0; i < observers.length; i++)
2453                 observers[i].createdOutput(this, section, outputName);
2454     }
2455 
2456     /**
2457      * Notify observers that a particular output has been completed.
2458      *
2459      * @param section The section that was completed.
2460      * @param outputName The name of the output.
2461      */
2462     private synchronized void notifyCompletedOutput(Section section, String outputName) {
2463         Observer[] observers = observersTable.get(this);
2464         if (observers != null)
2465             for (int i = 0; i < observers.length; i++)
2466                 observers[i].completedOutput(this, section, outputName);
2467     }
2468 
2469     /**
2470      * Notify all observers that new data has been written to some output.
2471      *
2472      * @param section The section being modified.
2473      * @param outputName The stream of the section that is being modified.
2474      * @param text The text that was added (appended).
2475      */
2476     private synchronized void notifyUpdatedOutput(Section section, String outputName, int start, int end, String text) {
2477         Observer[] observers = observersTable.get(this);
2478         if (observers != null)
2479             for (int i = 0; i < observers.length; i++)
2480                 observers[i].updatedOutput(this, section, outputName, start, end, text);
2481     }
2482 
2483     /**
2484      * Notify all observers that new data has been written to some output.
2485      *
2486      * @param section The section being modified.
2487      * @param outputName The stream of the section that is being modified.
2488      */
2489     private synchronized void notifyUpdatedOutput(Section section, String outputName, int start, int end,
2490                                                   char[] buf, int offset, int len) {
2491         Observer[] observers = observersTable.get(this);
2492         if (observers != null) {
2493             // only create string if there are really observers who want to see it
2494             String text = new String(buf, offset, len);
2495             for (int i = 0; i < observers.length; i++)
2496                 observers[i].updatedOutput(this, section, outputName, start, end, text);
2497         }
2498     }
2499 
2500     /**
2501      * Notify all observers that a property has been updated.
2502      *
2503      * @param key The key for the property that was modified.
2504      * @param value The new value for the property.
2505      */
2506     private synchronized void notifyUpdatedProperty(String key, String value) {
2507         Observer[] observers = observersTable.get(this);
2508         if (observers != null)
2509             for (int i = 0; i < observers.length; i++)
2510                 observers[i].updatedProperty(this, key, value);
2511     }
2512 
2513     /**
2514      * Notify observers the test has completed.
2515      */
2516     private synchronized void notifyCompleted() {
2517         // since there will be no more observer messages after this, there
2518         // is no need to keep any observers registered after we finish here
2519         // so get the observers one last time, and at the same time
2520         // remove them from the table
2521         Observer[] observers = observersTable.remove(this);
2522         if (observers != null) {
2523             for (int i = 0; i < observers.length; i++)
2524                 observers[i].completed(this);
2525             observersTable.remove(this);
2526         }
2527 
2528     }
2529 
2530     /**
2531      * @return Position of the specified section, or -1 if not found.
2532      */
2533     private synchronized int findSection(String name) {
2534         int location;
2535 
2536         if (sections == null || sections.length == 0) {
2537             return -1;
2538         }
2539 
2540         for (location = 0; location < sections.length; location++) {
2541             if (sections[location].getTitle().equals(name)) {
2542                 // found
2543                 break;
2544             }
2545         }   // for
2546 
2547         // loop exited because of counter, not a hit
2548         if (location == sections.length) {
2549             location = -1;
2550         }
2551 
2552         return location;
2553     }
2554 
2555     private void addToShrinkList() {
2556         synchronized (shrinkList) {
2557             // if this object is in the list; remove it;
2558             // if there are dead weak refs, remove them
2559             for (Iterator<WeakReference<TestResult>> iter = shrinkList.iterator(); iter.hasNext(); ) {
2560                 WeakReference<TestResult> wref = iter.next();
2561                 Object o = wref.get();
2562                 if (o == null || o == this)
2563                     iter.remove();
2564             }
2565             while (shrinkList.size() >= maxShrinkListSize) {
2566                 WeakReference<TestResult> wref = shrinkList.removeFirst();
2567                 TestResult tr = wref.get();
2568                 if (tr != null)
2569                     tr.shrink();
2570             }
2571             shrinkList.addLast(new WeakReference<>(this));
2572         }
2573     }
2574 
2575     /**
2576      * Tells the object that it can optimize itself for a small memory footprint.
2577      * Doing this may sacrifice performance when accessing object data.  This
2578      * only works on results that are immutable.
2579      */
2580     private synchronized void shrink() {
2581         if (isMutable()) {
2582             throw new IllegalStateException("Can't shrink a mutable test result!");
2583         }
2584 
2585         // Should ensure we have a resultsFile.
2586         sections = null;
2587 
2588         // NOTE: if either of these are discarded, it may be a good idea to
2589         //       optimize reload() to not read the section/stream data since
2590         //       a small property lookup could incur a huge overhead
2591         //props = null;         // works, may or may-not improve memory usage
2592         //desc = null;          // doesn't work in current implementation
2593     }
2594 
2595     // the following fields should be valid for all test results
2596     private File resultsFile;           // if set, location where test results are stored
2597     private Status execStatus;          // pre-compare result
2598     private String testURL;             // URL for this test, equal to the one in TD.getRootRelativeURL
2599     private long endTime = -1;          // when test finished
2600     private byte checksumState;         // checksum state
2601     // the following fields are candidates for shrinking although not currently done
2602     private TestDescription desc;       // test description for which this is the result
2603     private String[] props;             // table of values written during test execution
2604     private String[] env;
2605     // this field is cleared when the test result is shrunk
2606     private Section[] sections;         // sections of output written during test execution
2607     private int maxTROutputSize = 0;    // maximum output size for this test result
2608 
2609     // only valid when this TR is in a TRT, should remain when shrunk
2610     private TestResultTable.TreeNode parent;
2611 
2612     // because so few test results will typically be observed (e.g. at most one)
2613     // we don't use a per-instance array of observers, but instead associate any
2614     // such arrays here.
2615     private static Map<TestResult, Observer[]> observersTable = new Hashtable<>(16);
2616 
2617     /**
2618      * The name of the default output that all Sections are equipped with.
2619      */
2620     public static final String MESSAGE_OUTPUT_NAME = "messages";
2621 
2622     /**
2623      * The name of the default section that all TestResult objects are equipped with.
2624      */
2625     public static final String MSG_SECTION_NAME = "script_messages";
2626 
2627     /**
2628      * The name of the property that defines the test description file.
2629      */
2630     public static final String DESCRIPTION = "description";
2631 
2632     /**
2633      * The name of the property that defines the time at which the test
2634      * execution finished.
2635     */
2636     public static final String END = "end";
2637 
2638     /**
2639      * The name of the property that defines the environment name.
2640      */
2641     public static final String ENVIRONMENT = "environment";
2642 
2643     /**
2644      * The name of the property that defines the test execution status.
2645      */
2646     public static final String EXEC_STATUS = "execStatus";
2647 
2648     /**
2649      * The name of the property that defines the OS on which JT Harness
2650      * was running when the test was run.
2651      */
2652     public static final String JAVATEST_OS = "javatestOS";
2653 
2654     /**
2655      * The name of the property that defines the script that ran the test.
2656      */
2657     public static final String SCRIPT = "script";
2658 
2659     /**
2660      * The name of the property that defines the test output sections
2661      * that were recorded when the test ran.
2662      */
2663     public static final String SECTIONS = "sections";
2664 
2665     /**
2666      * The name of the property that defines the time at which the test
2667      * execution started.
2668     */
2669     public static final String START = "start";
2670 
2671     /**
2672      * The name of the property that defines the test for which this is
2673      * the result object.
2674     */
2675     public static final String TEST = "test";
2676 
2677     /**
2678      * The name of the property that defines which version of JT Harness
2679      * was used to run the test.
2680      */
2681     public static final String VERSION = "javatestVersion";
2682 
2683     /**
2684      * The name of the property that defines the work directory for the test.
2685      */
2686     public static final String WORK = "work";
2687 
2688     /**
2689      * The name of the property that defines the variety of harness in use.
2690      * Generally the full harness or the lite version.
2691      */
2692     public static final String VARIETY = "harnessVariety";
2693 
2694     /**
2695      * The name of the property that defines the type of class loader used when
2696      * running the harness (classpath mode or module mode generally).
2697      */
2698     public static final String LOADER = "harnessLoaderMode";
2699 
2700     /**
2701      * DateFormat, that is used to store date into TestResult
2702      */
2703     public static final DateFormat dateFormat =
2704             new SimpleDateFormat("EEE MMM dd HH:mm:ss zzz yyyy", Locale.US);
2705 
2706     static final String EXTN = ".jtr";
2707 
2708     private static final Status
2709         filesSame       = Status.passed("Output file and reference file matched"),
2710         filesDifferent  = Status.failed("Output file and reference file were different"),
2711         fileError       = Status.failed("Error occurred during comparison"),
2712         interrupted     = Status.failed("interrupted"),
2713         inProgress      = Status.notRun("Test running..."),
2714         incomplete      = Status.notRun("Section not closed, may be incomplete"),
2715         tdMismatch      = Status.notRun("Old test flushed, new test description located"),
2716         notRunStatus    = Status.notRun("");
2717 
2718     private static final String[] emptyStringArray = new String[0];
2719     private static final Section[] emptySectionArray = new Section[0];
2720 
2721     private static final String defaultClassDir = "classes";
2722 
2723     // info for reading/writing JTR files (version 1)
2724     private static final String JTR_V1_HEADER = "#Test Results";
2725     private static final String JTR_V1_SECTRESULT = "command result:";
2726     private static final String JTR_V1_TSTRESULT = "test result:";
2727 
2728     // info for reading/writing JTR files (version 2)
2729     private static final String JTR_V2_HEADER = "#Test Results (version 2)";
2730     private static final String JTR_V2_SECTION = "#section:";
2731     private static final String JTR_V2_CHECKSUM = "#checksum:";
2732     private static final String JTR_V2_TESTDESC = "#-----testdescription-----";
2733     private static final String JTR_V2_RESPROPS = "#-----testresult-----";
2734     private static final String JTR_V2_ENVIRONMENT = "#-----environment-----";
2735     private static final String JTR_V2_SECTRESULT = "result: ";
2736     private static final String JTR_V2_TSTRESULT = "test result: ";
2737     private static final String JTR_V2_SECTSTREAM = "----------";
2738 
2739     private static final String lineSeparator = System.getProperty("line.separator");
2740 
2741     private static final int DEFAULT_MAX_SHRINK_LIST_SIZE = 128;
2742     private static final int maxShrinkListSize =
2743         Integer.getInteger("javatest.numCachedResults", DEFAULT_MAX_SHRINK_LIST_SIZE).intValue();
2744     private static LinkedList<WeakReference<TestResult>> shrinkList = new LinkedList<>();
2745 
2746     private static final int DEFAULT_MAX_OUTPUT_SIZE = 100000;
2747     private static final int commonOutputSize =
2748         Integer.getInteger("javatest.maxOutputSize", DEFAULT_MAX_OUTPUT_SIZE).intValue();
2749 
2750     private static I18NResourceBundle i18n = I18NResourceBundle.getBundleForClass(TestResult.class);
2751 
2752     private static boolean debug = Boolean.getBoolean("debug." + TestResult.class.getName());
2753 
2754 }