/* * $Id$ * * Copyright (c) 1996, 2016, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the LICENSE file that accompanied this code. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ package com.sun.javatest; import java.io.*; import java.lang.ref.WeakReference; import java.nio.charset.StandardCharsets; import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.*; import com.sun.javatest.util.*; import com.sun.javatest.util.Properties; /** * The TestResult object encapsulates the results from a test. * Test results are formatted in sections of command output, * comments and sometimes "streams" of output (stdout for example). * Each of these sections is represented by a (@link TestResult.Section Section). * Instances of this class are mutable until the result of the section is * set or until the result of the test itself is set. * * Test results are stored in a structured text files. * The TestResult class serves as the API for accessing the various * components that make up the result of running a test. * The status is cached as its size is small and it is accessed often. * * This class and inner classes will throw IllegalStateExceptions if an * attempt is made to modify the any part of the object that has been * marked immutable. */ public class TestResult { /** * This exception is to report problems using TestResult objects. */ public static class Fault extends Exception { Fault(I18NResourceBundle i18n, String key) { super(i18n.getString(key)); } Fault(I18NResourceBundle i18n, String key, Object arg) { super(i18n.getString(key, arg)); } Fault(I18NResourceBundle i18n, String key, Object[] args) { super(i18n.getString(key, args)); } } /** * This exception is thrown if the JTR file cannot be found. */ public static class ResultFileNotFoundFault extends Fault { ResultFileNotFoundFault(I18NResourceBundle i18n, String key) { super(i18n, key); } ResultFileNotFoundFault(I18NResourceBundle i18n, String key, Object arg) { super(i18n, key, arg); } ResultFileNotFoundFault(I18NResourceBundle i18n, String key, Object[] args) { super(i18n, key, args); } } /** * This exception ay occur anytime the JTR file is being read from the filesystem. * To optimize memory usage, the contents of a TestResult object are sometimes * discarded and then loaded on demand from the JTR file. If a fault occurs * when reading the JTR file, this fault may occur. * * @see TestResult.ResultFileNotFoundFault */ public static class ReloadFault extends Fault { ReloadFault(I18NResourceBundle i18n, String key) { super(i18n, key); } ReloadFault(I18NResourceBundle i18n, String key, Object arg) { super(i18n, key, arg); } ReloadFault(I18NResourceBundle i18n, String key, Object[] args) { super(i18n, key, args); } } /** * An interface to observe activity in a TestResult as it is created. */ public interface Observer { /** * A new section has been created in the test result. * * @param tr The test result in which the section was created. * @param section The section that has been created */ public void createdSection(TestResult tr, Section section); /** * A section has been been completed in the test result. * * @param tr The test result containing the section. * @param section The section that has been completed. */ public void completedSection(TestResult tr, Section section); /** * New output has been created in a section of the test result. * * @param tr The test result containing the output. * @param section The section in which the output has been created. * @param outputName The name of the output. */ public void createdOutput(TestResult tr, Section section, String outputName); /** * Output has been completed in a section of the test result. * * @param tr The test result containing the output. * @param section The section in which the output has been completed. * @param outputName The name of the output. */ public void completedOutput(TestResult tr, Section section, String outputName); /** * The output for a section has been updated. * * @param tr The test result object being modified. * @param section The section in which the output is being produced. * @param outputName The name of the output. * @param start the start offset of the text that was changed * @param end the end offset of the text that was changed * @param text the text that replaced the specified range. */ public void updatedOutput(TestResult tr, Section section, String outputName, int start, int end, String text); /** * A property of the test result has been updated. * * @param tr The test result containing the property that was modified. * @param name The key for the property that was modified. * @param value The new value for the property. * */ public void updatedProperty(TestResult tr, String name, String value); /** * The test has completed, and the results are now immutable. * There will be no further observer calls. * @param tr The test result that has been completed. */ public void completed(TestResult tr); } /** * This "section" is the logical combination of a single action during test * execution. It is designed to hold multiple (or none) buffers of * output from test execution, such as stdout and stderr. In addition, * it has a "comment" field for tracking the test run itself (progress). * This output is identified by the MSG_SECTION_NAME identifier. */ public class Section { /** * Query if the section is still writable or not. * @return true if the section is still writable, and false otherwise */ public boolean isMutable() { synchronized (TestResult.this) { synchronized (this) { return (TestResult.this.isMutable() && this.result == inProgress); } } } /** * Find out what the result of the execution of this section was. * @return the result of the execution of this section * @see #setStatus */ public Status getStatus() { return result; } /** * Set the result of this section. This action makes this section * immutable. * * @param result The status to set as the result of this section of the test * @see #getStatus */ public void setStatus(Status result) { synchronized (TestResult.this) { synchronized (this) { checkMutable(); for (int i = 0; i < buffers.length; i++) { OutputBuffer b = buffers[i]; if (b instanceof WritableOutputBuffer) { WritableOutputBuffer wb = (WritableOutputBuffer)b; wb.getPrintWriter().close(); } } if (env == null) env = emptyStringArray; this.result = result; if (env == null) env = emptyStringArray; notifyCompletedSection(this); } } } /** * Get the title of this section, specified when the section * was created. * @return the title of this section */ public String getTitle() { return title; } /** * Get the appropriate to writer to access the default message field. * @return a Writer to access the default message field */ public PrintWriter getMessageWriter() { synchronized (TestResult.this) { synchronized (this) { checkMutable(); // if it is mutable, it must have a message stream, // which will be the first entry return buffers[0].getPrintWriter(); } } } /** * Find out how many output buffers this section has inside it. * * @return The number of output buffers in use (>=0). */ public synchronized int getOutputCount() { return buffers.length; } /** * Add a new output buffer to the section; get PrintWriter access to it. * * @param name The symbolic name that will identify this new stream. * @return A PrintWriter that gives access to the new stream. */ public PrintWriter createOutput(String name) { if (name == null) throw new NullPointerException(); synchronized (TestResult.this) { synchronized (this) { checkMutable(); OutputBuffer b = new WritableOutputBuffer(name); buffers = DynamicArray.append(buffers, b); notifyCreatedOutput(this, name); return b.getPrintWriter(); } } } /** * Get the content that was written to a specified output stream. * @param name the name of the stream in question * @return All the data that was written to the specified output, * or null if nothing has been written. */ public String getOutput(String name) { if (name == null) throw new NullPointerException(); synchronized (TestResult.this) { synchronized (this) { OutputBuffer b = findOutputBuffer(name); return (b == null ? null : b.getOutput()); } } } /** * Find out the symbolic names of all the streams in this section. You * can use getOutputCount to discover the number of items in * this enumeration (not a thread safe activity in the strictest * sense of course). * * @return A list of strings which are the symbolic names of the streams in this section. * @see #getOutputCount */ public synchronized String[] getOutputNames() { String[] names = new String[buffers.length]; for (int i = 0; i < buffers.length; i++) { names[i] = buffers[i].getName(); if (names[i] == null) throw new IllegalStateException("BUFFER IS BROKEN"); } return names; } /** * Removes any data added to the named output up to this point, resetting * it to an empty state. * @param name The output name to erase the content of. * @since 4.2.1 */ public synchronized void deleteOutputData(String name) { if (name == null) throw new NullPointerException(); synchronized (TestResult.this) { synchronized (this) { OutputBuffer b = findOutputBuffer(name); if (b != null && b instanceof WritableOutputBuffer) ((WritableOutputBuffer)b).deleteAllOutput(); } } } // ---------- PACKAGE PRIVATE ---------- public Section(String title) { if (title == null) throw new NullPointerException(); if (title.indexOf(' ') != -1) throw new IllegalArgumentException("space invalid in section title"); this.title = title; result = inProgress; } /** * Could be used to reconstruct the section from a stream. * Reads from the source until it finds a section header. This is a JTR * version 2 method, don't use it for version 1 files. The object * immediately immutable upon return from this constructor. * * @throws ReloadFault Probably an error while parsing the input stream. */ Section(BufferedReader in) throws IOException, ReloadFault { String line = in.readLine(); // find top of section and process it while (line != null) { if (line.startsWith(JTR_V2_SECTION)) { title = extractSlice(line, 0, ":", null); break; } else // don't know what this line is, may be empty line = in.readLine(); } if (title == null) throw new ReloadFault(i18n, "rslt.noSectionTitle"); if (title.equals(MSG_SECTION_NAME)) { // use standard internal copy of string title = MSG_SECTION_NAME; } while ((line = in.readLine()).startsWith(JTR_V2_SECTSTREAM)) { OutputBuffer b = new FixedOutputBuffer(line, in); buffers = DynamicArray.append(buffers, b); } // if not in the message section, line should have the section result if (title != MSG_SECTION_NAME) { if (line != null) { if (line.startsWith(JTR_V2_SECTRESULT)) result = Status.parse(line.substring(JTR_V2_SECTRESULT.length())); else throw new ReloadFault(i18n, "rslt.badLine", line); } if (result == null) // no test result throw new ReloadFault(i18n, "rslt.noSectionResult"); } } void save(Writer out) throws IOException { out.write(JTR_V2_SECTION + getTitle()); out.write(lineSeparator); for (int index = 0; index < buffers.length; index++) { String text = buffers[index].getOutput(); int numLines = 0; int numBackslashes = 0; int numNonASCII = 0; boolean needsFinalNewline = false; boolean needsEscape; // scan for newlines and characters requiring escapes for (int i = 0; i < text.length(); i++) { char c = text.charAt(i); if (c < 32) { if (c == '\n') numLines++; else if (c != '\t' && c != '\r') numNonASCII++; } else if (c < 127) { if (c == '\\') numBackslashes++; } else numNonASCII++; } needsEscape = (numBackslashes > 0 || numNonASCII > 0); // Check the text ends with a final newline ('\n', not line.separator) // Note this must match the check when reading the text back in, // when we also check for just '\n' and not line.separator, because // line.separator now, and line.separator then, might be different. if (text.length() != 0 && !text.endsWith("\n")) { needsFinalNewline = true; numLines++; } out.write(JTR_V2_SECTSTREAM); out.write(buffers[index].getName()); out.write(":"); out.write('('); out.write(String.valueOf(numLines)); out.write('/'); if (needsEscape) { // count one per character, plus an additional one per \ (written as "\ \") and an // additional 5 per nonASCII (written as "\ u x x x x") out.write(String.valueOf(text.length() + numBackslashes + 5*numNonASCII)); } else out.write(String.valueOf(text.length())); out.write(')'); if (needsEscape) out.write('*'); out.write(JTR_V2_SECTSTREAM); out.write(lineSeparator); if (needsEscape) { for (int i = 0; i < text.length(); i++) { char c = text.charAt(i); if (32 <= c && c < 127 && c != '\\') out.write(c); else { switch (c) { case '\n': case '\r': case '\t': out.write(c); break; case '\\': out.write("\\\\"); break; default: out.write("\\u"); out.write(Character.forDigit((c >> 12) & 0xF, 16)); out.write(Character.forDigit((c >> 8) & 0xF, 16)); out.write(Character.forDigit((c >> 4) & 0xF, 16)); out.write(Character.forDigit((c >> 0) & 0xF, 16)); break; } } } } else out.write(text); if (needsFinalNewline) out.write(lineSeparator); } // the default message section does not need a result line if (getTitle() != MSG_SECTION_NAME) { out.write(JTR_V2_SECTRESULT + result.toString()); out.write(lineSeparator); } out.write(lineSeparator); } /** * Reload an output block. This method is called while reloading * a test result and so bypasses the normal immutability checks. */ synchronized void reloadOutput(String name, String data) { if (name.equals(MESSAGE_OUTPUT_NAME)) name = MESSAGE_OUTPUT_NAME; OutputBuffer b = new FixedOutputBuffer(name, data); buffers = DynamicArray.append(buffers, b); } /** * Reload the status. This method is called while reloading * a test result and so bypasses the normal immutability checks. */ synchronized void reloadStatus(Status s) { result = s; } // ---------- PRIVATE ---------- private void checkMutable() { if (!isMutable()) throw new IllegalStateException("This section of the test result is now immutable."); } private synchronized void makeOutputImmutable(OutputBuffer b, String name, String output) { for (int i = 0; i < buffers.length; i++) { if (buffers[i] == b) { buffers[i] = new FixedOutputBuffer(name, output); return; } } } private synchronized OutputBuffer findOutputBuffer(String name) { // search backwards // may help in some backward compatibility cases since the most // recent stream with that name will be found // performance of the search will still be constant for (int i = buffers.length-1; i >= 0 ; i--) { if (name.equals(buffers[i].getName())) return buffers[i]; } return null; } private OutputBuffer[] buffers = new OutputBuffer[0]; private String title; private Status result; private class FixedOutputBuffer implements OutputBuffer { FixedOutputBuffer(String name, String output) { if (name == null || output == null) throw new NullPointerException(); this.name = name; this.output = output; } public String getName() { return name; } public String getOutput() { return output; } public PrintWriter getPrintWriter() { throw new IllegalStateException("This section is immutable"); } FixedOutputBuffer(String header, BufferedReader in) throws ReloadFault { String nm = extractSlice(header, JTR_V2_SECTSTREAM.length(), null, ":"); if (nm == null) throw new ReloadFault(i18n, "rslt.noOutputTitle"); if (nm.equals(MESSAGE_OUTPUT_NAME )) nm = MESSAGE_OUTPUT_NAME; try { int lines; int chars; boolean needsEscape; try { int start = JTR_V2_SECTSTREAM.length(); lines = Integer.parseInt(extractSlice(header, start, "(", "/")); chars = Integer.parseInt(extractSlice(header, start, "/", ")")); int rp = header.indexOf(")", start); if (rp >= 0 && rp < header.length() - 2) needsEscape = (header.charAt(rp + 1) == '*'); else needsEscape = false; } catch (NumberFormatException e) { // fatal parsing error throw new ReloadFault(i18n, "rslt.badHeaderVersion", e); } StringBuffer buff = new StringBuffer(chars); if (needsEscape) { for (int i = 0; i < chars; i++) { int c = in.read(); if (c == -1) throw new ReloadFault(i18n, "rslt.badEOF"); else if (c == '\\') { c = in.read(); i++; if (c == 'u') { c = Character.digit((char)in.read(), 16) << 12; c += Character.digit((char)in.read(), 16) << 8; c += Character.digit((char)in.read(), 16) << 4; c += Character.digit((char)in.read(), 16); i += 4; } // else drop through (for \\) } buff.append((char)c); } } else { char[] data = new char[Math.min(4096, chars)]; int charsRead = 0; while (charsRead < chars) { int n = in.read(data, 0, Math.min(data.length, chars-charsRead)); // sanity check, may be truncated file if (n < 0) { throw new ReloadFault(i18n, "rslt.badRuntimeErr", new Object[] {resultsFile, Integer.toString(n)}); } buff.append(data, 0, n); charsRead += n; } } /*NEW while (true) { int c = in.read(); switch (c) { case -1: throw new ReloadFault(i18n, "rslt.badEOF"); case '\\': if (needEscape) { c = in.read(); if (c == 'u') { c = Character.digit((char)in.read(), 16) << 12; c += Character.digit((char)in.read(), 16) << 8; c += Character.digit((char)in.read(), 16) << 4; c += Character.digit((char)in.read(), 16); } // else drop through (for \\) } buff.append((char)c); } } */ name = nm; output = buff.toString(); if (buff.length() > 0 && buff.charAt(buff.length() - 1) != '\n') { int c = in.read(); if (c == '\r') c = in.read(); if (c != '\n') { System.err.println("TR.badChars: output=" + (output.length() < 32 ? output : output.substring(0, 9) + " ... " + output.substring(output.length() - 10) )); System.err.println("TR.badChars: '" + ((char)c) + "' (" + c + ")"); throw new ReloadFault(i18n, "rslt.badChars", name); } } } catch (IOException e) { // not enough data probably fatal parsing error throw new ReloadFault(i18n, "rslt.badFile", e); } } private final String name; private final String output; } private class WritableOutputBuffer extends Writer implements OutputBuffer { WritableOutputBuffer(String name) { super(TestResult.this); if (name == null) throw new NullPointerException(); this.name = name; output = new StringBuffer(); pw = new LockedWriter(this, TestResult.this); } public String getName() { return name; } public String getOutput() { return new String(output); } public PrintWriter getPrintWriter() { return pw; } public void write(char[] buf, int offset, int len) throws IOException { if (output == null) throw new IOException("stream has been closed"); int end = output.length(); output.append(buf, offset, len); // want to avoid creating the string buf(offset..len) // since likely case is no observers notifyUpdatedOutput(Section.this, name, end, end, buf, offset, len); int maxOutputSize = maxTROutputSize > 0 ? maxTROutputSize : commonOutputSize; if (output.length() > maxOutputSize) { int overflowEnd = maxOutputSize/3; if (overflowed) { // output.delete(overflowStart, overflowEnd); // JDK 1.1--start String s = output.toString(); output = new StringBuffer(s.substring(0, overflowStart) + s.substring(output.length()-overflowEnd)); // JDK 1.1--end notifyUpdatedOutput(Section.this, name, overflowStart, overflowEnd, ""); } else { String OVERFLOW_MESSAGE = "\n\n...\n" + "Output overflow:\n" + "JT Harness has limited the test output to the text to that\n" + "at the beginning and the end, so that you can see how the\n" + "test began, and how it completed.\n" + "\n" + "If you need to see more of the output from the test,\n" + "set the system property javatest.maxOutputSize to a higher\n" + "value. The current value is " + maxOutputSize + "\n...\n\n"; overflowStart = maxOutputSize/3; //output.replace(overflowStart, maxOutputSize*2/3, OVERFLOW_MESSAGE); // JDK 1.1--start String s = output.toString(); output = new StringBuffer(s.substring(0, overflowStart) + OVERFLOW_MESSAGE + s.substring(overflowEnd)); // JDK 1.1--end notifyUpdatedOutput(Section.this, name, overflowStart, overflowEnd, OVERFLOW_MESSAGE); overflowStart += OVERFLOW_MESSAGE.length(); overflowed = true; } } } public void flush() { //no-op } public void deleteAllOutput() { pw.flush(); output.setLength(0); overflowStart = -1; overflowed = false; } public void close() { makeOutputImmutable(this, name, new String(output)); notifyCompletedOutput(Section.this, name); } private boolean overflowed; private int overflowStart; private final String name; private /*final*/ StringBuffer output; // can't easily be final in JDK 1.1 because need to reassign to it private final PrintWriter pw; } } private class LockedWriter extends PrintWriter { public LockedWriter(Writer out, Object theLock) { super(out); lock = theLock; } } // Conceptually, this belongs in Section, but that is not legal Java. // (It is accepted in 1.1.x; rejected by 1.2) private interface OutputBuffer { String getName(); String getOutput(); PrintWriter getPrintWriter(); } // ------------------------- PUBLIC CONSTRUCTORS ------------------------- /** * Construct a test result object that will be built as the test runs. * The status string will be "running..." rather than "not run". * * @param td The test description to base this new object on. Cannot be * null. */ public TestResult(TestDescription td) { desc = td; execStatus = inProgress; testURL = desc.getRootRelativeURL(); createSection(MSG_SECTION_NAME); props = emptyStringArray; // null implies it was discarded, not empty } /** * Reconstruct the results of a previously run test. * * @param workDir Work directory in which the tests were run * @param td Description of the test that was run * @throws TestResult.Fault if there is a problem recreating the results * from the appropriate file in the work directory */ public TestResult(TestDescription td, WorkDirectory workDir) throws Fault { desc = td; testURL = desc.getRootRelativeURL(); execStatus = inProgress; reloadFromWorkDir(workDir); } /** * Reconstruct the results of a previously run test. * * @param file File that the results have been stored into. * @throws TestResult.ReloadFault if there is a problem recreating the results * from the given file * @throws TestResult.ResultFileNotFoundFault if there is a problem locating * the given file */ public TestResult(File file) throws ResultFileNotFoundFault, ReloadFault { resultsFile = file; reload(); testURL = desc.getRootRelativeURL(); execStatus = Status.parse(PropertyArray.get(props, EXEC_STATUS)); } /** * Reconstruct the results of a previously run test. * * @param workDir The work directory where previous results for the guven * test can be found. * @param workRelativePath The path to the JTR to reload, relative to the * workdir. * @throws TestResult.Fault if there is a problem recreating the results * from the given file */ public TestResult(WorkDirectory workDir, String workRelativePath) throws Fault { //resultsFile = workDir.getFile(workRelativePath.replace('/', File.separatorChar)); resultsFile = workDir.getFile(workRelativePath); reload(); testURL = desc.getRootRelativeURL(); execStatus = Status.parse(PropertyArray.get(props, EXEC_STATUS)); } /** * Create a temporary test result for which can be handed around * in situations where a reasonable test result can't be created. * * @param td Description of the test * @param s Status to associate with running the test... presumed * to be of the Status.FAILED type. */ public TestResult(TestDescription td, Status s) { desc = td; testURL = desc.getRootRelativeURL(); resultsFile = null; execStatus = s; props = emptyStringArray; } /** * Create a placeholder TestResult for a test that has not yet been run. * * @param td The test description for the test * @return A test result that indicates that the test has not yet been run */ public static TestResult notRun(TestDescription td) { return new TestResult(td, notRunStatus); } //------------------------ MODIFIER METHODS ------------------------------ /** * Create a new section inside this test result. * * @param name The symbolic name for this new section. * @return The new section that was created. */ public synchronized TestResult.Section createSection(String name) { if (!isMutable()) { throw new IllegalStateException( "This TestResult is no longer mutable!"); } Section section = new Section(name); sections = DynamicArray.append(sections, section); notifyCreatedSection(section); // avoid creating output (which will cause observer messages) // before the createdSection has been notified section.createOutput(TestResult.MESSAGE_OUTPUT_NAME); return section; } /** * Set the environment used by this test. When the test is run, * those entries in the environment that are referenced are noted; * those entries will be recorded here in the test result object. * @param environment the test environment used by this test. * @see #getEnvironment */ public synchronized void setEnvironment(TestEnvironment environment) { if (!isMutable()) { throw new IllegalStateException( "This TestResult is no longer mutable!"); } for (Iterator i = environment.elementsUsed().iterator(); i.hasNext(); ) { TestEnvironment.Element elem = i.next(); // this is stunningly inefficient and should be fixed env = PropertyArray.put(env, elem.getKey(), elem.getValue()); } } /** * Set the result of this test. This action makes this object immutable. * If a result comparison is needed, it will be done in here. * @param stat A status object representing the outcome of the test * @see #getStatus */ public synchronized void setStatus(Status stat) { if (!isMutable()) { throw new IllegalStateException( "This TestResult is no longer mutable!"); } if (stat == null) { throw new IllegalArgumentException( "TestResult status cannot be set to null!"); } // close out message section sections[0].setStatus(null); execStatus = stat; if (execStatus == inProgress) execStatus = interrupted; // verify integrity of status in all sections for (int i = 0; i < sections.length; i++) { if (sections[i].isMutable()) { sections[i].setStatus(incomplete); } } props = PropertyArray.put(props, SECTIONS, StringArray.join(getSectionTitles())); props = PropertyArray.put(props, EXEC_STATUS, execStatus.toString()); // end time now required // mainly for writing in the TRC for the Last Run Filter if (PropertyArray.get(props, END) == null) { props = PropertyArray.put(props, END, formatDate(new Date())); } // this object is now immutable notifyCompleted(); } /** * Add a new property value to this TestResult. * * @param name The name of the property to be updated. * @param value The new value of the specified property. */ public synchronized void putProperty(String name, String value) { // check mutability if (!isMutable()) { throw new IllegalStateException( "Cannot put property, the TestResult is no longer mutable!"); } props = PropertyArray.put(props, name, value); notifyUpdatedProperty(name, value); } /** * Sets the maximum output size for the current TestResult. * The value will be used instead of the value specified * by the system property javatest.maxOutputSize. * @param size the maximum number of characters. */ public synchronized void setMaxOutputSize(int size){ if (!isMutable()) { throw new IllegalStateException( "This TestResult is no longer mutable!"); } maxTROutputSize = size; } /** * Reconstruct the results of a previously run test. * * @param workDir Work directory in which the tests were run * @throws TestResult.Fault if an error occurs while reloading the results */ public void reloadFromWorkDir(WorkDirectory workDir) throws Fault { // check mutability if (!isMutable()) { throw new IllegalStateException( "Cannot reload results, the TestResult is no longer mutable!"); } try { resultsFile = workDir.getFile(getWorkRelativePath()); props = null; sections = null; execStatus = null; reload(new InputStreamReader(new FileInputStream(resultsFile), StandardCharsets.UTF_8)); // this next line is dubious since the execStatus should have // been set during the reload execStatus = Status.parse(PropertyArray.get(props, EXEC_STATUS)); } catch (FileNotFoundException e) { props = emptyStringArray; env = emptyStringArray; sections = emptySectionArray; execStatus = Status.notRun("no test result file found"); } catch (IOException e) { props = emptyStringArray; env = emptyStringArray; sections = emptySectionArray; execStatus = Status.error("error opening result file: " + e); throw new Fault(i18n, "rslt.badFile", e.toString()); } catch (Fault f) { props = emptyStringArray; env = emptyStringArray; sections = emptySectionArray; execStatus = Status.error(f.getMessage()); throw f; } } //----------ACCESS FUNCTIONS (MISC)----------------------------------------- /** * A code indicating that no checksum was found in a .jtr file. * @see #getChecksumState */ public static final int NO_CHECKSUM = 0; /** * A code indicating that an invalid checksum was found in a .jtr file. * @see #getChecksumState */ public static final int BAD_CHECKSUM = 1; /** * A code indicating that a good checksum was found in a .jtr file. * @see #getChecksumState */ public static final int GOOD_CHECKSUM = 2; /** * The number of different checksum states (none, good, bad). */ public static final int NUM_CHECKSUM_STATES = 3; /** * Get info about the checksum in this object. * @return a value indicating the validity or otherwise of the checksum * found while reading this result object. * @see #NO_CHECKSUM * @see #BAD_CHECKSUM * @see #GOOD_CHECKSUM */ public byte getChecksumState() { return checksumState; } /** * A way to write comments about the test execution into the results. * * @return If this is null, then the object is in a state in which it * does not accept new messages. */ public PrintWriter getTestCommentWriter() { return sections[0].getMessageWriter(); } /** * Get the test name, as given by the test URL defined by * TestDescription.getRootRelativeURL(). This method always * returns a useful string, representing the test name. * * @return the name of the test for which this is the result object * @see TestDescription#getRootRelativeURL */ public String getTestName() { return testURL; } /** * Check whether this test result can be reloaded from a file. * This method does not validate the contents of the file. * @return true if the result file for this object can be read */ public boolean isReloadable() { return (resultsFile != null && resultsFile.canRead()); } /** * Check whether this object has been "shrunk" to reduce its * memory footprint. If it has, some or all of the data will have * to be reloaded. This method is somewhat * orthogonal to isReloadable() and should not be used as a * substitute. * * @return True if this object is currently incomplete, false otherwise. * @see #isReloadable */ public boolean isShrunk() { if (!isMutable() && (desc == null || props == null || env == null || (sections == null && execStatus != inProgress))) return true; else return false; } /** * Get the description of the test from which this result was created. * Depending on how the test result was created, this information may * not be immediately available, and may be recreated from the test * result file. * * @return the test description for this test result object * @throws TestResult.Fault if there is a problem recreating the description * from the results file. */ public synchronized TestDescription getDescription() throws Fault { if (desc == null) { // reconstitute description (probably from file) reload(); } return desc; } /* * Get the title of this test. This info originally comes from the test * description, but is saved in the .jtr file as well. * * @deprecated Please query the test description for info. public String getTitle() { // hmm slight copout; would like to make sure never null in the first place String title = desc.getParameter("title"); if (title == null) title = td.getRootRelativeURL(); } */ /** * Get the path name for the results file for this test, relative to the * work directory. The internal separator is '/'. * @return the path name for the results file for this test, * relative to the work directory */ public String getWorkRelativePath() { return getWorkRelativePath(testURL); } /** * Get the name, if any, for the result file for this object. * The path information contains platform specific path separators. * @return the name, if any, for the result file for this object */ public File getFile() { return resultsFile; } public void resetFile() { resultsFile = null; } /** * Get the path name for the results file for a test, relative to the * work directory. The internal separator is '/'. * @param td the test description for the test in question * @return the path name for the results file for a test, relative to the * work directory */ public static String getWorkRelativePath(TestDescription td) { String baseURL = td.getRootRelativePath(); // add in uniquifying id if String id = td.getParameter("id"); return getWorkRelativePath(baseURL, id); } /** * Get the path name for the results file for a test, relative to the * work directory. The internal separator is '/'. * * @param testURL May not be null; * @return The work relative path of the JTR for this test. Null if the * given URL is null. */ public static String getWorkRelativePath(String testURL) { int pound = testURL.lastIndexOf("#"); if (pound == -1) // no test id return getWorkRelativePath(testURL, null); else return getWorkRelativePath(testURL.substring(0, pound), testURL.substring(pound + 1)); } /** * Get the path name for the results file for a test, relative to the * work directory. The internal separator is '/'. * * @param baseURL May not be null; * @param testId The test identifier that goes with the URL. This may be null. * @return The work relative path of the JTR for this test. Null if the * given URL is null. */ public static String getWorkRelativePath(String baseURL, String testId) { StringBuffer sb = new StringBuffer(baseURL); // strip off extension stripExtn: for (int i = sb.length() - 1; i >= 0; i--) { switch (sb.charAt(i)) { case '.': sb.setLength(i); break stripExtn; case '/': break stripExtn; } } // add in uniquifying id if if (testId != null) { sb.append('_'); sb.append(testId); } sb.append(EXTN); return sb.toString(); } /** * Get the keys of the properties that this object has stored. * @return the keys of the properties that this object has stored */ public synchronized Enumeration getPropertyNames() { return PropertyArray.enumerate(props); } /** * Get the value of a property of this test result. * * @param name The name of the property to be retrieved. * @return The value corresponding to the property name, null if not * found. * @throws TestResult.Fault if there is a problem * recreating data from the results file. */ public synchronized String getProperty(String name) throws Fault { if (props == null) { // reconstitute properties // this may result in a Fault, which is okay reload(); } return PropertyArray.get(props, name); } /** * Get a copy of the environment that this object has stored. * @return a copy of the environment that this object has stored * @throws TestResult.Fault if there is a problem * recreating data from the results file. * @see #setEnvironment */ public synchronized Map getEnvironment() throws Fault { if (env == null) { // reconstitute environment // this may result in a Fault, which is okay reload(); } return PropertyArray.getProperties(env); } /** * Get the parent node in the test result table that * contains this test result object. * @return the parent node in the test result table that * contains this test result object. */ public TestResultTable.TreeNode getParent() { return parent; } /** * Set the parent node in the test result table that * contains this test result object. * @param p the parent node in the test result table that * contains this test result object. * @see #getParent */ void setParent(TestResultTable.TreeNode p) { parent = p; } //----------ACCESS FUNCTIONS (TEST STATUS)---------------------------------- /** * Determine if the test result object is still mutable. * Test results are only mutable while they are being created, up to * the point that the final status is set. * @return true if the test result object is still mutable, * and false otherwise */ public synchronized boolean isMutable() { // must be mutable during reload (internal operation) // mutable as long as possible, to allow max time for writing log messages return (execStatus == inProgress); } /** * Get the status for this test. * @return the status for this test * @see #setStatus */ public synchronized Status getStatus() { return execStatus; } //----------ACCESS METHODS (TEST OUTPUT)---------------------------------- /** * Find out how many sections this test result contains. * * @return The number of sections in this result. */ public synchronized int getSectionCount() { if (sections != null) { return sections.length; } else if (PropertyArray.get(props, SECTIONS) != null) { return parseSectionCount(PropertyArray.get(props, SECTIONS)); } else { // hum, test props are never discarded, so we have no sections return 0; } } /** * Get the section specified by index. * Remember that index 0 is the default message section. * * @param index The index of the section to be retrieved. * @return The requested section. Will be null if the section does not exist. * @throws TestResult.ReloadFault Will occur if an error is encountered when reloading * JTR data. This may be the result of a corrupt or missing JTR file. * @see #MSG_SECTION_NAME */ public synchronized Section getSection(int index) throws ReloadFault { Section target; if (sections == null && execStatus != inProgress) { // try to reload from file try { reload(); } catch (ReloadFault f) { throw f; } catch (Fault f) { throw new ReloadFault(i18n, "rslt.badFile", f.getMessage()); } } if (index >= sections.length) { target = null; } else { target = sections[index]; } return target; } /** * Get the titles of all sections in this test result. * A null result probably indicates that there are no sections. This is * improbable since most test result object automatically have one section. * * @return The titles, one at a time in the array. Null if the titles * do not exist or could not be determined. */ public synchronized String[] getSectionTitles() { if (props == null) { try { reload(); } catch (Fault f) { // should this maybe be a JavaTestError? return null; } } // look for what we need from easiest to hardest source String names = PropertyArray.get(props, SECTIONS); if (names != null) { // it is cached return StringArray.split(names); } else if (sections != null) { // TR is not immutable yet, probably int numSections = getSectionCount(); String[] data = new String[numSections]; for (int i = 0; i < numSections; i++) { data[i] = sections[i].getTitle(); } return data; } else { // hum, bad. No sections exist and this data isn't cached // the test probably has not run return null; } } /** * Check if this file is or appears to be a result (.jtr) file, * according to its filename extension. * @param f the file to be checked * @return true if this file is or appears to be a result (.jtr) file. */ public static boolean isResultFile(File f) { String p = f.getPath(); return (p.endsWith(EXTN)); } /** * Writes the TestResult into a version 2 jtr file. * * @param workDir The work directory in which to write the results * @param backupPolicy a policy object defining what to do if a file * already exists with the same name as that which is about to be written. * @throws IllegalStateException This will occur if you attempt to write a result * which is still mutable. * @throws IOException Occurs when the output file cannot be created or written to. * Under this condition, this object will change it status to an error. */ public synchronized void writeResults(WorkDirectory workDir, BackupPolicy backupPolicy) throws IOException { if (isMutable()) throw new IllegalStateException("This TestResult is still mutable - set the status!"); // could attempt a reload() I suppose if (props == null) props = emptyStringArray; String wrp = getWorkRelativePath(desc).replace('/', File.separatorChar); resultsFile = workDir.getFile(wrp); File resultsDir = resultsFile.getParentFile(); resultsDir.mkdirs(); // ensure directory created for .jtr file File tempFile = createTempFile(workDir, backupPolicy); try { writeResults(tempFile, backupPolicy); } finally { if (tempFile.exists()) tempFile.delete(); } } /** * Create a temporary file to which the results can be written, before being renamed * to its real name. */ // don't use File.createTempFile because of issues with the internal locking there private File createTempFile(WorkDirectory workDir, BackupPolicy backupPolicy) throws IOException { final int MAX_TRIES = 100; // absurdly big limit, but a limit nonetheless for (int i = 0; i < MAX_TRIES; i++) { File tempFile = new File(resultsFile.getPath() + "." + i + ".tmp"); if (tempFile.createNewFile()) return tempFile; } throw new IOException("could not create temp file for " + resultsFile + ": too many tries"); } /** * Write the results to a temporary file, and when done, rename it to resultsFile */ private void writeResults(File tempFile, BackupPolicy backupPolicy) throws IOException { Writer out; try { out = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(tempFile), StandardCharsets.UTF_8)); } catch (IOException e) { execStatus = Status.error("Problem writing result file for test: " + getTestName()); resultsFile = null; // file not successfully written after all throw e; } try { // redundant, is done in setResult // needed though if setResult isn't being called props = PropertyArray.put(props, EXEC_STATUS, execStatus.toString()); // file header out.write(JTR_V2_HEADER); out.write(lineSeparator); // date and time out.write("#" + (new Date()).toString()); out.write(lineSeparator); // checksum header and data //out.write(JTR_V2_CHECKSUM); //out.write(Long.toHexString(computeChecksum())); //out.write(lineSeparator); /* if (debug) { // debugging code out.write("# debug: test desc checksum: "); out.write(Long.toHexString(computeChecksum(desc))); out.write(lineSeparator); for (Iterator iter = desc.getParameterKeys(); iter.hasNext(); ) { // don't rely on enumeration in a particular order // so simply add the checksum products together String KEY = (String) (iter.next()); out.write("# debug: test desc checksum key " + KEY + ": "); out.write(Long.toHexString(computeChecksum(KEY) * computeChecksum(desc.getParameter(KEY)))); out.write(lineSeparator); } out.write("# debug: test env checksum: "); if (env == null) out.write("null"); else out.write(Long.toHexString(computeChecksum(env))); out.write(lineSeparator); out.write("# debug: test props checksum: "); out.write(Long.toHexString(computeChecksum(props))); out.write(lineSeparator); out.write("# debug: test sections checksum: "); out.write(Long.toHexString(computeChecksum(sections))); out.write(lineSeparator); for (int I = 0; I < sections.length; I++) { out.write("# debug: test section[" + I + "] checksum: "); out.write(Long.toHexString(computeChecksum(sections[I]))); out.write(lineSeparator); String[] NAMES = sections[I].getOutputNames(); for (int J = 0; J < NAMES.length; J++) { out.write("# debug: test section[" + I + "] name=" + NAMES[J] + " checksum: "); out.write(Long.toHexString(computeChecksum(NAMES[J]))); out.write(lineSeparator); out.write("# debug: test section[" + I + "] name=" + NAMES[J] + " output checksum: "); out.write(Long.toHexString(computeChecksum(sections[I].getOutput(NAMES[J])))); out.write(lineSeparator); } } }*/ // description header and data out.write(JTR_V2_TESTDESC); out.write(lineSeparator); Map tdProps = new HashMap<>(); desc.save(tdProps); PropertyArray.save(PropertyArray.getArray(tdProps), out); out.write(lineSeparator); // test environment header and data if (env != null) { out.write(JTR_V2_ENVIRONMENT); out.write(lineSeparator); PropertyArray.save(env, out); out.write(lineSeparator); } // test result props header and data out.write(JTR_V2_RESPROPS); out.write(lineSeparator); PropertyArray.save(props, out); out.write(lineSeparator); // get sections into memory // I hope the out stream is not the same as the resultFile! if (sections == null) { throw new JavaTestError("Cannot write test result - it contains no sections."); } for (int i = 0; i < sections.length; i++) { sections[i].save(out); } out.write(lineSeparator); out.write(JTR_V2_TSTRESULT); out.write(execStatus.toString()); out.write(lineSeparator); out.close(); } // try catch (IOException e) { // This exception could be raised when trying to create the directory // for the test results; opening the results file, or closing it. execStatus = Status.error("Write to temp. JTR file failed (old JTR intact): " + tempFile.getPath()); resultsFile = null; // file not successfully written after all throw e; } // catch try { backupPolicy.backupAndRename(tempFile, resultsFile); // now that it has been successfully written out, make the object // a candidate for shrinking addToShrinkList(); } // try catch (IOException e) { // This exception could be raised when trying to create the directory // for the test results; opening the results file, or closing it. execStatus = Status.error("Problem writing result file: " + resultsFile.getPath()); resultsFile = null; // file not successfully written after all throw e; } // catch } // -----observer methods --------------------------------------------------- /** * Add an observer to watch this test result for changes. * @param obs the observer to be added */ public synchronized void addObserver(Observer obs) { if (isMutable()) { Observer[] observers = observersTable.get(this); if (observers == null) observers = new Observer[0]; observers = DynamicArray.append(observers, obs); observersTable.put(this, observers); } } /** * Remove an observer that was previously added. * @param obs the observer to be removed */ public synchronized void removeObserver(Observer obs) { Observer[] observers = observersTable.get(this); if (observers == null) return; observers = DynamicArray.remove(observers, obs); if (observers == null) observersTable.remove(this); else observersTable.put(this, observers); } /** * Gets the time when the test was completed, or at least the time * when it's final status was set. Be aware that if the information is * not available in memory, it will be loaded from disk. * * @return Time when this test acquired its final status setting. * @see #setStatus * @see java.util.Date */ public long getEndTime() { if (endTime < 0) { try { String datestr = PropertyArray.get(props, END); if (datestr == null) { // this may be more expensive because it can cause a // reload from disk try { datestr = getProperty(END); } catch (Fault f) { } } if (datestr != null) { Date date = parseDate(datestr); endTime = date.getTime(); } else { // info not available } } catch (ParseException e) { } } return endTime; } /** * Parse the date format used for timestamps, such as the start/stop timestamp. * @param s The string containing the date to be restored. * @see #formatDate */ public static synchronized Date parseDate(String s) throws ParseException { return dateFormat.parse(s); } /** * Format the date format used for timestamps, such as the start/stop timestamp. * @param d The date object to be formatted into a string. * @see #parseDate */ public static synchronized String formatDate(Date d) { return dateFormat.format(d); } // ----- PACKAGE METHODS --------------------------------------------------- /** * Read a single minimal TestResult from a .jts stream. * The stream is not closed. * @deprecated JTS files are no longer supported TestResult(WorkDirectory workDir, DataInputStream in) throws IOException { workRelativePath = in.readUTF(); // ** temp. fix ** XXX // make sure the path is in URL form with forward slashes // in the future all paths should already be of this form (TestDescription) int index = workRelativePath.indexOf('/'); if (index == -1) workRelativePath = workRelativePath.replace('\\', '/'); resultsFile = workDir.getFile(workRelativePath.replace('/', File.separatorChar)); title = in.readUTF(); int esc = in.readByte(); String esr = in.readUTF(); execStatus = new Status(esc, esr); boolean defIsExec = in.readBoolean(); if (!defIsExec) { // have to read these, per protocol int dsc = in.readByte(); String dsr = in.readUTF(); //ignore dsc, dsr; they used to go in defStatus } } */ /** * Read a single minimal TestResult which is capable of reloading itself. * None of the parameters may be null. * * @param url The full URL of this test, including test id. * @param workDir The work directory location, platform specfic path. * @param status The status that will be found in the JTR. * @throws JavaTestError Will be thrown if any params are null. */ TestResult(String url, WorkDirectory workDir, Status status) { if (url == null) throw new JavaTestError(i18n, "rslt.badTestUrl"); if (workDir == null) throw new JavaTestError(i18n, "rslt.badWorkdir"); if (status == null) throw new JavaTestError(i18n, "rslt.badStatus"); testURL = url; resultsFile = workDir.getFile(getWorkRelativePath()); execStatus = status; } /** * Read a single minimal TestResult which is capable of reloading itself. * None of the parameters may be null. * * @param url The full URL of this test, including test id. * @param workDir The work directory location, platform specific path. * @param status The status that will be found in the JTR. * @param endTime The time when that test finished execution. * @throws JavaTestError Will be thrown if any params are null. * @see #getEndTime() */ TestResult(String url, WorkDirectory workDir, Status status, long endTime) { if (url == null) throw new JavaTestError(i18n, "rslt.badTestUrl"); if (workDir == null) throw new JavaTestError(i18n, "rslt.badWorkdir"); if (status == null) throw new JavaTestError(i18n, "rslt.badStatus"); testURL = url; resultsFile = workDir.getFile(getWorkRelativePath()); execStatus = status; this.endTime = endTime; } void shareStatus(Map[] tables) { execStatus = shareStatus(tables, execStatus); } /* * @deprecated JTS files no longer supported void writeSummary(DataOutputStream out) throws IOException { out.writeUTF(workRelativePath); out.writeUTF(title); out.writeByte(execStatus.getType()); out.writeUTF(execStatus.getReason()); out.writeBoolean(true); // defStatus == execStatus } */ String[] getTags() { // Script or someone else could possibly do this w/the observer if (sections == null) { return null; } Vector tagV = new Vector<>(sections.length * 2); for (int i = 0; i < sections.length; i++) { String[] names = sections[i].getOutputNames(); for (int j = 0; j < names.length; j++) { tagV.addElement(names[j]); } // inner for } // outer for String[] tagA = new String[tagV.size()]; tagV.copyInto(tagA); return tagA; } /** * Insert a test description into this test results. * This will only work if the test description is currently not available. * The name in the test description must match the name of this test. * @param td The new test description, a null value will have no effect. * @see #isShrunk() * @throws IllegalStateException If the state of this object fobiu */ void setTestDescription(TestDescription td) { if (td == null) return; String name = td.getRootRelativeURL(); if (!testURL.equals(name)) throw new IllegalStateException(); if (desc != null) { // compare if possible if (!desc.equals(td)) { // test descriptions are not the same // accept new TD, reset this TR // reset status to a special one execStatus = tdMismatch; desc = td; props = emptyStringArray; resultsFile = null; env = emptyStringArray; sections = emptySectionArray; if (isMutable()) createSection(MSG_SECTION_NAME); } else { // TDs are equal, no action, drop thru and return } } else { desc = td; } } // ----- PRIVATE METHODS --------------------------------------------------- /** * @deprecated Use the Section API to accomplish your task. */ private static Reader getLastRefOutput(TestResult tr) { try { Section lastBlk = tr.getSection(tr.getSectionCount() - 1); return new StringReader(lastBlk.getOutput("ref")); } catch (ReloadFault f) { // not the best, but this method is deprecated and hopefully never // called return null; } } private long computeChecksum() { long cs = 0; cs = cs * 37 + computeChecksum(desc); // in JT2.1.1a, environment was not included in checksum, // so allow that for backward compatibility String jtv = PropertyArray.get(props, VERSION); if (env != null) { if (jtv == null || !jtv.equals("JT_2.1.1a")) cs = cs * 37 + computeChecksum(env); } cs = cs * 37 + computeChecksum(props); if (sections != null) cs = cs * 37 + computeChecksum(sections); cs = cs * 37 + execStatus.getType() + computeChecksum(execStatus.getReason()); return Math.abs(cs); // ensure +ve, to avoid sign issues! } private static long computeChecksum(TestDescription td) { long cs = 0; for (Iterator i = td.getParameterKeys(); i.hasNext(); ) { // don't rely on enumeration in a particular order // so simply add the checksum products together String key = (i.next()); cs += computeChecksum(key) * computeChecksum(td.getParameter(key)); } return cs; } private static long computeChecksum(Section[] sections) { long cs = sections.length; for (int i = 0; i < sections.length; i++) { cs = cs * 37 + computeChecksum(sections[i]); } return cs; } private static long computeChecksum(Section s) { long cs = computeChecksum(s.getTitle()); String[] names = s.getOutputNames(); for (int i = 0; i [] tables, Status s) { int type = s.getType(); String reason = s.getReason(); Status result = tables[type].get(reason); if (result == null) { tables[type].put(reason, s); result = s; } return result; } // ------------------------ OBSERVER MAINTENANCE ------------------------- /** * Notify observers that a new section has been created. * * @param section The section that was created. */ private synchronized void notifyCreatedSection(Section section) { Observer[] observers = observersTable.get(this); if (observers != null) for (int i = 0; i < observers.length; i++) observers[i].createdSection(this, section); } /** * Notify observers that a section has been completed. * * @param section The section that was completed. */ private synchronized void notifyCompletedSection(Section section) { Observer[] observers = observersTable.get(this); if (observers != null) for (int i = 0; i < observers.length; i++) observers[i].completedSection(this, section); } /** * Notify observers that new output is being created. * * @param section The section that was created. * @param outputName The name of the output. */ private synchronized void notifyCreatedOutput(Section section, String outputName) { Observer[] observers = observersTable.get(this); if (observers != null) for (int i = 0; i < observers.length; i++) observers[i].createdOutput(this, section, outputName); } /** * Notify observers that a particular output has been completed. * * @param section The section that was completed. * @param outputName The name of the output. */ private synchronized void notifyCompletedOutput(Section section, String outputName) { Observer[] observers = observersTable.get(this); if (observers != null) for (int i = 0; i < observers.length; i++) observers[i].completedOutput(this, section, outputName); } /** * Notify all observers that new data has been written to some output. * * @param section The section being modified. * @param outputName The stream of the section that is being modified. * @param text The text that was added (appended). */ private synchronized void notifyUpdatedOutput(Section section, String outputName, int start, int end, String text) { Observer[] observers = observersTable.get(this); if (observers != null) for (int i = 0; i < observers.length; i++) observers[i].updatedOutput(this, section, outputName, start, end, text); } /** * Notify all observers that new data has been written to some output. * * @param section The section being modified. * @param outputName The stream of the section that is being modified. */ private synchronized void notifyUpdatedOutput(Section section, String outputName, int start, int end, char[] buf, int offset, int len) { Observer[] observers = observersTable.get(this); if (observers != null) { // only create string if there are really observers who want to see it String text = new String(buf, offset, len); for (int i = 0; i < observers.length; i++) observers[i].updatedOutput(this, section, outputName, start, end, text); } } /** * Notify all observers that a property has been updated. * * @param key The key for the property that was modified. * @param value The new value for the property. */ private synchronized void notifyUpdatedProperty(String key, String value) { Observer[] observers = observersTable.get(this); if (observers != null) for (int i = 0; i < observers.length; i++) observers[i].updatedProperty(this, key, value); } /** * Notify observers the test has completed. */ private synchronized void notifyCompleted() { // since there will be no more observer messages after this, there // is no need to keep any observers registered after we finish here // so get the observers one last time, and at the same time // remove them from the table Observer[] observers = observersTable.remove(this); if (observers != null) { for (int i = 0; i < observers.length; i++) observers[i].completed(this); observersTable.remove(this); } } /** * @return Position of the specified section, or -1 if not found. */ private synchronized int findSection(String name) { int location; if (sections == null || sections.length == 0) { return -1; } for (location = 0; location < sections.length; location++) { if (sections[location].getTitle().equals(name)) { // found break; } } // for // loop exited because of counter, not a hit if (location == sections.length) { location = -1; } return location; } private void addToShrinkList() { synchronized (shrinkList) { // if this object is in the list; remove it; // if there are dead weak refs, remove them for (Iterator> iter = shrinkList.iterator(); iter.hasNext(); ) { WeakReference wref = iter.next(); Object o = wref.get(); if (o == null || o == this) iter.remove(); } while (shrinkList.size() >= maxShrinkListSize) { WeakReference wref = shrinkList.removeFirst(); TestResult tr = wref.get(); if (tr != null) tr.shrink(); } shrinkList.addLast(new WeakReference<>(this)); } } /** * Tells the object that it can optimize itself for a small memory footprint. * Doing this may sacrifice performance when accessing object data. This * only works on results that are immutable. */ private synchronized void shrink() { if (isMutable()) { throw new IllegalStateException("Can't shrink a mutable test result!"); } // Should ensure we have a resultsFile. sections = null; // NOTE: if either of these are discarded, it may be a good idea to // optimize reload() to not read the section/stream data since // a small property lookup could incur a huge overhead //props = null; // works, may or may-not improve memory usage //desc = null; // doesn't work in current implementation } // the following fields should be valid for all test results private File resultsFile; // if set, location where test results are stored private Status execStatus; // pre-compare result private String testURL; // URL for this test, equal to the one in TD.getRootRelativeURL private long endTime = -1; // when test finished private byte checksumState; // checksum state // the following fields are candidates for shrinking although not currently done private TestDescription desc; // test description for which this is the result private String[] props; // table of values written during test execution private String[] env; // this field is cleared when the test result is shrunk private Section[] sections; // sections of output written during test execution private int maxTROutputSize = 0; // maximum output size for this test result // only valid when this TR is in a TRT, should remain when shrunk private TestResultTable.TreeNode parent; // because so few test results will typically be observed (e.g. at most one) // we don't use a per-instance array of observers, but instead associate any // such arrays here. private static Map observersTable = new Hashtable<>(16); /** * The name of the default output that all Sections are equipped with. */ public static final String MESSAGE_OUTPUT_NAME = "messages"; /** * The name of the default section that all TestResult objects are equipped with. */ public static final String MSG_SECTION_NAME = "script_messages"; /** * The name of the property that defines the test description file. */ public static final String DESCRIPTION = "description"; /** * The name of the property that defines the time at which the test * execution finished. */ public static final String END = "end"; /** * The name of the property that defines the environment name. */ public static final String ENVIRONMENT = "environment"; /** * The name of the property that defines the test execution status. */ public static final String EXEC_STATUS = "execStatus"; /** * The name of the property that defines the OS on which JT Harness * was running when the test was run. */ public static final String JAVATEST_OS = "javatestOS"; /** * The name of the property that defines the script that ran the test. */ public static final String SCRIPT = "script"; /** * The name of the property that defines the test output sections * that were recorded when the test ran. */ public static final String SECTIONS = "sections"; /** * The name of the property that defines the time at which the test * execution started. */ public static final String START = "start"; /** * The name of the property that defines the test for which this is * the result object. */ public static final String TEST = "test"; /** * The name of the property that defines which version of JT Harness * was used to run the test. */ public static final String VERSION = "javatestVersion"; /** * The name of the property that defines the work directory for the test. */ public static final String WORK = "work"; /** * The name of the property that defines the variety of harness in use. * Generally the full harness or the lite version. */ public static final String VARIETY = "harnessVariety"; /** * The name of the property that defines the type of class loader used when * running the harness (classpath mode or module mode generally). */ public static final String LOADER = "harnessLoaderMode"; /** * DateFormat, that is used to store date into TestResult */ public static final DateFormat dateFormat = new SimpleDateFormat("EEE MMM dd HH:mm:ss zzz yyyy", Locale.US); static final String EXTN = ".jtr"; private static final Status filesSame = Status.passed("Output file and reference file matched"), filesDifferent = Status.failed("Output file and reference file were different"), fileError = Status.failed("Error occurred during comparison"), interrupted = Status.failed("interrupted"), inProgress = Status.notRun("Test running..."), incomplete = Status.notRun("Section not closed, may be incomplete"), tdMismatch = Status.notRun("Old test flushed, new test description located"), notRunStatus = Status.notRun(""); private static final String[] emptyStringArray = new String[0]; private static final Section[] emptySectionArray = new Section[0]; private static final String defaultClassDir = "classes"; // info for reading/writing JTR files (version 1) private static final String JTR_V1_HEADER = "#Test Results"; private static final String JTR_V1_SECTRESULT = "command result:"; private static final String JTR_V1_TSTRESULT = "test result:"; // info for reading/writing JTR files (version 2) private static final String JTR_V2_HEADER = "#Test Results (version 2)"; private static final String JTR_V2_SECTION = "#section:"; private static final String JTR_V2_CHECKSUM = "#checksum:"; private static final String JTR_V2_TESTDESC = "#-----testdescription-----"; private static final String JTR_V2_RESPROPS = "#-----testresult-----"; private static final String JTR_V2_ENVIRONMENT = "#-----environment-----"; private static final String JTR_V2_SECTRESULT = "result: "; private static final String JTR_V2_TSTRESULT = "test result: "; private static final String JTR_V2_SECTSTREAM = "----------"; private static final String lineSeparator = System.getProperty("line.separator"); private static final int DEFAULT_MAX_SHRINK_LIST_SIZE = 128; private static final int maxShrinkListSize = Integer.getInteger("javatest.numCachedResults", DEFAULT_MAX_SHRINK_LIST_SIZE).intValue(); private static LinkedList> shrinkList = new LinkedList<>(); private static final int DEFAULT_MAX_OUTPUT_SIZE = 100000; private static final int commonOutputSize = Integer.getInteger("javatest.maxOutputSize", DEFAULT_MAX_OUTPUT_SIZE).intValue(); private static I18NResourceBundle i18n = I18NResourceBundle.getBundleForClass(TestResult.class); private static boolean debug = Boolean.getBoolean("debug." + TestResult.class.getName()); }