mirror of https://github.com/OpenIdentityPlatform/OpenDJ.git

davidely
22.40.2007 0e81a89428812436ad35285700c8fae614a7b92e
1.  Wrapped TestNG suite runner with our own (SuiteRunner.java).  When there is an uncaught exception in the tests, this gives us the stack trace and forces the tests to exit.  This should make it easier to track down OutOfMemoryErrors in the future.

2. Added our own listener for the tests. This does the following
* As soon as a test fails, it prints it out all of the info to the screen. Test method, error message, stacktrace, parameters
* At the end of the tests, it summarizes all of the tests that failed.
* It generates a new test report in text format. The html report generator was a real memory hog.

3. Changes to build.xml
* Test output is suppressed by default.
* The coverage report is generated even if the tests failed.
* Exposed the ability to rerun only the failed tests with -Dtest.failed=true.
* The 'runtests' target prints out information about additional test options.
* Two small fixes so that ant doesn't recompile files that haven't changed.
* Removed tabs. This was mostly in the status panel code.
* Reverted the -Xmx256M temporary fix for the unit tests.


1 files deleted
3 files added
5 files modified
809 ■■■■ changed files
opends/build-tools/src/org/opends/build/tools/CoverageDiff.java 52 ●●●● patch | view | raw | blame | history
opends/ext/testng/CHANGES.txt 74 ●●●●● patch | view | raw | blame | history
opends/ext/testng/lib/testng-5.1-jdk15.jar patch | view | raw | blame | history
opends/ext/testng/lib/testng-5.4-jdk15.jar patch | view | raw | blame | history
opends/ext/testng/src.zip patch | view | raw | blame | history
opends/tests/unit-tests-testng/src/server/org/opends/server/DirectoryServerTestCase.java 105 ●●●●● patch | view | raw | blame | history
opends/tests/unit-tests-testng/src/server/org/opends/server/SuiteRunner.java 46 ●●●●● patch | view | raw | blame | history
opends/tests/unit-tests-testng/src/server/org/opends/server/TestCaseUtils.java 63 ●●●●● patch | view | raw | blame | history
opends/tests/unit-tests-testng/src/server/org/opends/server/TestListener.java 469 ●●●●● patch | view | raw | blame | history
opends/build-tools/src/org/opends/build/tools/CoverageDiff.java
@@ -77,7 +77,7 @@
  private File emmaDataPath;
  private File outputPath;
  private File diffPath;
  private String diffPath;
  private File svnPath;
  public void setEmmaDataPath(String file)
@@ -90,9 +90,9 @@
    outputPath = new File(file);
  }
  public void setDiffPath(String file)
  public void setDiffPath(String diffArgs)
  {
    diffPath = new File(file);
    diffPath = diffArgs;
  }
  public void setSvnPath(String file)
@@ -110,10 +110,21 @@
    enabled = bol.toLowerCase().equals("true");
  }
  public void execute() throws BuildException
  public void execute() throws BuildException {
    try {
      innerExecute();
    } catch (BuildException e) {
      throw e;
    } catch (Throwable e) {
      e.printStackTrace();
    }
  }
  private void innerExecute() throws BuildException
  {
    long start = System.currentTimeMillis();
    verboseOut("Starting to execute coveragediff.");
    verboseOut("diffPath='" + diffPath +"'");
    if(emmaDataPath == null)
    {
      throw new BuildException("emmaDataPath attribute is not set. It must be set to the path of the EMMA data directory");
@@ -350,7 +361,7 @@
    statsTable.setClass ("it");
    {
      HTMLTable.IRow row = statsTable.newRow ();
      row.newCell ().setText ("base directory:", true);
      row.newCell ().setText ("svn diff arg(s):", true);
      row.newCell ().setText ("" + diffPath.toString(), true);
      row = statsTable.newRow ();
@@ -461,6 +472,7 @@
    modCoverage[DEL_LINES] = 0.0;
    String fileHeader = diffFile.get(0);
    verboseOut("fileHeader: " + diffFile);
    //Try to get the package information if its a Java file
    File srcFilePath = new File(fileHeader.substring(7));
@@ -483,8 +495,14 @@
    String firstFileLine = diffFile.get(2);
    String secondFileLine = diffFile.get(3);
    verboseOut("firstFileLine=" + firstFileLine);
    verboseOut("secondFileLine=" + secondFileLine);
    String revisionStr = "unknown";
    // Skip over binary files
    if (firstFileLine.contains("Cannot display")) {
      return;
    }
    HTMLTable srcTable = null;
@@ -572,7 +590,7 @@
    final IElement itemname = IElement.Factory.create (Tag.SPAN);
    {
      itemname.setText (toRelativePath(srcFilePath.toString()), true);
      itemname.setText (srcFilePath.toString(), true);
      itemname.setClass ("in");
    }
@@ -866,7 +884,7 @@
      if(nameHREF != null)
      {
        final String fullHREFName = anchor ? "#".concat (nameHREF) : nameHREF;
        nameCell.add(new HyperRef(fullHREFName, toRelativePath(fileName), true));
        nameCell.add(new HyperRef(fullHREFName, fileName, true));
      }
      else
      {
@@ -933,25 +951,7 @@
    }
  }
  private String createHREF(String name)
  {
    if(name == null)
    {
      return null;
    }
    name = name.replaceAll("[/,\\,(,),.]", "_");
    return name;
  }
  private String toRelativePath(String file)
  {
    return file.substring(diffPath.toString().length()+1);
  }
  // How does this get enabled?
  // Enable this with -Dtest.diff.verbose=true from the commandline
  private void verboseOut(Object msg)
  {
    if (verbose)
opends/ext/testng/CHANGES.txt
@@ -1,7 +1,75 @@
===========================================================================
5.4
TODO
    :Introduce ITestFactory, deprecate @Factory
    :Introduce Annotation Transformers
Fixed: Ant task issue with paths containing spaces
Added: for @BeforeGroups and @AfterGroups specifying the groups() attribute will auto-include the method
             into those groups by default (previously you had to also provide the value() attribute).
Added: the load @Tests (invocationCount + threadPoolSize) are triggered simultaneous
Fixed: reports are correctly displaying the thread info
Added: @DataProvider name defaults to method name
Added: support for remote protocol to pass parameter information
Fixed: TextReporter logs information about the parameters of the test methods
Fixed: concurrency issue in JUnitXMLReporter
Fixed: output of JUnitXMLReporter must be CDATA
Fixed: XML unsupported annotations/parallel attribute values are reported
Eclipse plug-in
Fixed: groups with multi-attribute javadoc annotations
Fixed: consistent behavior for dependsOnMethods
Fixed: consistent behavior for tests with dependsOnGroups (a warning is emitted)
Fixed: consistent merge of configuration arguments when an existing launch configuration exists
===========================================================================
5.3
Fixed: use a single instance of bsh.Interpreter
Added: @Before/@AfterMethod can declare a java.lang.reflect.Method parameter to be informed about the @Test method
Fixed: super classes must not be listed in testng-failures.xml
Fixed: parallel attribute must not appear if empty or null in testng-failures.xml
Fixed: parsing for javadoc annotations is done on request only
Added: improved multiple suite summary page report
Added: -target option deprecated in favor of -annotations javadoc|jdk
Fixed: filesets in the ant task didn't work if the paths have spaces in them
Fixed: Before/After Suite were behaving wrong in parallel execution
Added: A generic/extensible RemoteTestNG was added to the core
Fixed: Before/AfterGroup-s were behaving wrong when using invocationCount, dataProvider and threadPoolSize
Fixed: improved support for running different annotation type tests in the same suite
Fixed: testng-failed.xml was generated even if there were no failures/skipps
Fixed: -usedefaultlisteners was wrongly passed to JVM instead of TestNG options
Added: Attribute dataProviderClass for @Test and @testng.test
Fixed: Forgot to account for cases where both invocationCount and DataProviders are present
Fixed: AfterGroups were invoked out of order with invocationCount and DataProviders
Fixed: Reporter.getOutput() returned an empty array if a timeOut was specified
Added: testng.xml now supports <suite-files>
Added: ant task can receive several listeners
Fixed: TESTNG-109 Skipped tests with expected exceptions are reported as failures
Added: ant task can now select the parallel mode for running tests
Fixed: ant task correctly deals with empty groups and excludedgroups parameters
Added: ant task can override default suite and test names
Added: comand line support for setting parallel mode, suite and test names
Eclipse plug-in
Added: Support for configuring per project usedefaultlisteners
Added: Contextual drop-down menu on failures tab of the TestNG view to enable running/debugging method failure only
Added: Suppport for configuring per project TestNG jar usage (project provided one or plugin provided one)
===========================================================================
5.2
Added: "-usedefaultlisteners true/false" to command line and ant
Added: EmailableReporter (from Paul Mendelson)
Added: parallel can now be "methods" or "tests". Boolean version deprecated
Added: TestNGAntTask now uses the @ syntax to invoke TestNG
Added: Command line understands @ syntax
Added: JUnitConverter uses the new syntax
Added: -groups to JUnitConverter
Fixed: Throw proper exception when a DataProvider declares parameters
Added: completely revamped JUnit support (should run all kind of JUnit tests)
Fixed: TESTNG-40 (Bug in testng-failed.xml generation)
Fixed: TESTNG-106 (Failed "@BeforeSuite" method just skipps the last test in xml-file)
Fixed: Success on 0 tests (http://forums.opensymphony.com/thread.jspa?threadID=41213)
Eclipse plug-in
Added: TESTNG-105 Automaticaly define TESTNG_HOME classpath variable
===========================================================================
5.1
opends/ext/testng/lib/testng-5.1-jdk15.jar
Binary files differ
opends/ext/testng/lib/testng-5.4-jdk15.jar
Binary files differ
opends/ext/testng/src.zip
Binary files differ
opends/tests/unit-tests-testng/src/server/org/opends/server/DirectoryServerTestCase.java
@@ -29,16 +29,6 @@
import org.testng.annotations.AfterSuite;
import org.testng.annotations.BeforeSuite;
import java.io.PrintStream;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.logging.ConsoleHandler;
import java.util.logging.Handler;
import java.util.logging.Logger;
import java.util.logging.LogManager;
import org.opends.server.types.NullOutputStream;
/**
 * This class defines a base test case that should be subclassed by all
 * unit tests used by the Directory Server.
@@ -47,105 +37,14 @@
 * have them include the class name.
 */
public abstract class DirectoryServerTestCase {
  // The set of loggers for which the console logger has been disabled.
  private HashMap<Logger,Handler> disabledLogHandlers;
  // The print stream to use for printing error messages.
  private PrintStream errorStream;
  // The original System.err print stream.
  private PrintStream originalSystemErr;
  // The original System.out print stream.
  private PrintStream originalSystemOut;
  @BeforeSuite
  public final void suppressOutput() {
    String suppressStr = System.getProperty("org.opends.test.suppressOutput");
    if ((suppressStr != null) && suppressStr.equalsIgnoreCase("true"))
    {
      System.setOut(NullOutputStream.printStream());
      System.setErr(NullOutputStream.printStream());
      errorStream = NullOutputStream.printStream();
      LogManager logManager = LogManager.getLogManager();
      Enumeration<String> loggerNames = logManager.getLoggerNames();
      while (loggerNames.hasMoreElements())
      {
        String loggerName = loggerNames.nextElement();
        Logger logger = logManager.getLogger(loggerName);
        for (Handler h : logger.getHandlers())
        {
          if (h instanceof ConsoleHandler)
          {
            disabledLogHandlers.put(logger, h);
            logger.removeHandler(h);
            break;
          }
        }
      }
    }
    TestCaseUtils.suppressOutput();
  }
  @AfterSuite
  public final void shutdownServer() {
    TestCaseUtils.shutdownServer("The current test suite has finished.");
    System.setOut(originalSystemOut);
    System.setErr(originalSystemErr);
    errorStream = originalSystemErr;
    for (Logger l : disabledLogHandlers.keySet())
    {
      Handler h = disabledLogHandlers.get(l);
      l.addHandler(h);
    }
    disabledLogHandlers.clear();
    TestCaseUtils.unsupressOutput();
  }
  /**
   * Creates a new instance of this test case with the provided name.
   */
  protected DirectoryServerTestCase() {
    this.errorStream = System.err;
    disabledLogHandlers = new HashMap<Logger,Handler>();
    originalSystemOut   = System.out;
    originalSystemErr   = System.err;
  }
  /**
   * Prints the provided message to the error stream, prepending the
   * fully-qualified class name.
   *
   * @param message
   *          The message to be printed to the error stream.
   */
  public final void printError(String message) {
    errorStream.print(getClass().getName());
    errorStream.print(" -- ");
    errorStream.println(message);
  }
  /**
   * Prints the stack trace for the provided exception to the error
   * stream.
   *
   * @param exception
   *          The exception to be printed to the error stream.
   */
  public final void printException(Throwable exception) {
    exception.printStackTrace(errorStream);
  }
  /**
   * Specifies the error stream to which messages will be printed.
   *
   * @param errorStream
   *          The error stream to which messages will be printed.
   */
  public final void setErrorStream(PrintStream errorStream) {
    this.errorStream = errorStream;
  }
}
opends/tests/unit-tests-testng/src/server/org/opends/server/SuiteRunner.java
New file
@@ -0,0 +1,46 @@
/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License, Version 1.0 only
 * (the "License").  You may not use this file except in compliance
 * with the License.
 *
 * You can obtain a copy of the license at
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE
 * or https://OpenDS.dev.java.net/OpenDS.LICENSE.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE.  If applicable,
 * add the following below this CDDL HEADER, with the fields enclosed
 * by brackets "[]" replaced with your own identifying * information:
 *      Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 *
 *
 *      Portions Copyright 2007 Sun Microsystems, Inc.
 */
package org.opends.server;
import org.testng.TestNG;
import static org.opends.server.TestCaseUtils.originalSystemErr;
/**
 * This class wraps TestNG so that we can force the process to exit if there
 * is an uncaught exception (e.g. OutOfMemoryError).
 */
public class SuiteRunner {
  public static void main(String[] args) {
    try {
      TestNG.main(args);
    } catch (Throwable e) {
      originalSystemErr.println("TestNG.main threw an expected exception:");
      e.printStackTrace(originalSystemErr);
      System.exit(TestNG.HAS_FAILURE);
    }
  }
}
opends/tests/unit-tests-testng/src/server/org/opends/server/TestCaseUtils.java
@@ -33,6 +33,13 @@
import java.io.*;
import java.util.List;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Enumeration;
import java.util.Map;
import java.util.logging.Logger;
import java.util.logging.Handler;
import java.util.logging.LogManager;
import java.util.logging.ConsoleHandler;
import java.net.ServerSocket;
import java.net.InetSocketAddress;
import java.net.SocketException;
@@ -51,6 +58,7 @@
import org.opends.server.types.FilePermission;
import org.opends.server.types.InitializationException;
import org.opends.server.types.OperatingSystem;
import org.opends.server.types.NullOutputStream;
import static org.testng.Assert.*;
@@ -651,7 +659,7 @@
  public static String makeLdif(String... lines) {
    StringBuilder buffer = new StringBuilder();
    for (int i = 0; i < lines.length; i++) {
      buffer.append(lines[i]).append("\n");
      buffer.append(lines[i]).append(EOL);
    }
    return buffer.toString();
  }
@@ -731,4 +739,57 @@
      // Ignore it.
    }
  }
  // ---------------------------------------------------------------------------
  // ---------------------------------------------------------------------------
  // ---------------------------------------------------------------------------
  // The set of loggers for which the console logger has been disabled.
  private final static Map<Logger, Handler> disabledLogHandlers = new HashMap<Logger,Handler>();
  /** The original System.err print stream.  Use this if you absolutely
   *  must write something to System.err. */
  public final static PrintStream originalSystemErr = System.err;
  /** The original System.out print stream.  Use this if you absolutely
   *  must write something to System.out. */
  public final static PrintStream originalSystemOut = System.out;
  public synchronized static void suppressOutput() {
    String suppressStr = System.getProperty("org.opends.test.suppressOutput");
    if ((suppressStr != null) && suppressStr.equalsIgnoreCase("true"))
    {
      System.setOut(NullOutputStream.printStream());
      System.setErr(NullOutputStream.printStream());
      LogManager logManager = LogManager.getLogManager();
      Enumeration<String> loggerNames = logManager.getLoggerNames();
      while (loggerNames.hasMoreElements())
      {
        String loggerName = loggerNames.nextElement();
        Logger logger = logManager.getLogger(loggerName);
        for (Handler h : logger.getHandlers())
        {
          if (h instanceof ConsoleHandler)
          {
            disabledLogHandlers.put(logger, h);
            logger.removeHandler(h);
            break;
          }
        }
      }
    }
  }
  public synchronized static void unsupressOutput() {
    System.setOut(originalSystemOut);
    System.setErr(originalSystemErr);
    for (Logger l : disabledLogHandlers.keySet())
    {
      Handler h = disabledLogHandlers.get(l);
      l.addHandler(h);
    }
    disabledLogHandlers.clear();
  }
}
opends/tests/unit-tests-testng/src/server/org/opends/server/TestListener.java
New file
@@ -0,0 +1,469 @@
/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License, Version 1.0 only
 * (the "License").  You may not use this file except in compliance
 * with the License.
 *
 * You can obtain a copy of the license at
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE
 * or https://OpenDS.dev.java.net/OpenDS.LICENSE.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE.  If applicable,
 * add the following below this CDDL HEADER, with the fields enclosed
 * by brackets "[]" replaced with your own identifying * information:
 *      Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 *
 *
 *      Portions Copyright 2007 Sun Microsystems, Inc.
 */
package org.opends.server;
import org.testng.TestListenerAdapter;
import org.testng.IReporter;
import org.testng.ISuite;
import org.testng.ITestResult;
import org.testng.IClass;
import org.testng.ITestNGMethod;
import org.testng.ITestContext;
import org.testng.xml.XmlSuite;
import static org.opends.server.util.ServerConstants.EOL;
import static org.opends.server.TestCaseUtils.originalSystemErr;
import java.util.List;
import java.util.LinkedHashMap;
import java.util.Collection;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.io.PrintStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileNotFoundException;
/**
 * This class is our replacement for the test results that TestNG generates.
 *   It prints out test to the console as they happen.
 *   It
 *
 */
public class TestListener extends TestListenerAdapter implements IReporter {
  private final StringBuilder _bufferedTestFailures = new StringBuilder();
  public static final String REPORT_FILE_NAME = "results.txt";
  // This is used to communicate with build.xml.  So that even when a test
  // fails, we can do the coverage report before failing the build.
  public static final String ANT_TESTS_FAILED_FILE_NAME = ".tests-failed-marker";
  private static final String DIVIDER_LINE = "-------------------------------------------------------------------------------" + EOL;
  public void onStart(ITestContext testContext) {
    super.onStart(testContext);
    // Delete the previous report if it's there.
    new File(testContext.getOutputDirectory(), REPORT_FILE_NAME).delete();
  }
  public void generateReport(List<XmlSuite> xmlSuites, List<ISuite> suites, String outputDirectory) {
    File reportFile = new File(outputDirectory, REPORT_FILE_NAME);
    writeReportToFile(reportFile);
    writeReportToScreen(reportFile);
    writeAntTestsFailedMarker(outputDirectory);
  }
  private void writeAntTestsFailedMarker(String outputDirectory) {
    // Signal 'ant' that all of the tests passed by removing this
    // special file.
    if (countTestsWithStatus(ITestResult.FAILURE) == 0) {
      new File(outputDirectory, ANT_TESTS_FAILED_FILE_NAME).delete();
    }
  }
  private void writeReportToFile(File reportFile) {
    PrintStream reportStream = null;
    try {
      reportStream = new PrintStream(new FileOutputStream(reportFile));
    } catch (FileNotFoundException e) {
      originalSystemErr.println("Could not open " + reportFile + " for writing.  Will write the unit test report to the console instead.");
      e.printStackTrace(originalSystemErr);
      reportStream = originalSystemErr;
    }
    reportStream.println(center("UNIT TEST REPORT"));
    reportStream.println(center("----------------") + EOL);
    reportStream.println("Finished at: " + (new Date()));
    reportStream.println("# Test clases: " + _classResults.size());
    reportStream.println("# Test methods: " + countTestMethods());
    reportStream.println("# Tests passed: " + countTestsWithStatus(ITestResult.SUCCESS));
    reportStream.println("# Tests failed: " + countTestsWithStatus(ITestResult.FAILURE));
    reportStream.println(EOL + DIVIDER_LINE + DIVIDER_LINE + EOL + EOL);
    reportStream.println(center("FAILED TESTS"));
    reportStream.println(EOL + EOL);
    reportStream.println(_bufferedTestFailures);
    reportStream.println(EOL + DIVIDER_LINE + DIVIDER_LINE + EOL);
    reportStream.println(getTimingInfo());
    reportStream.close();
  }
  private String getFqMethod(ITestResult result) {
    IClass cls = result.getTestClass();
    ITestNGMethod method = result.getMethod();
    return cls.getName() + "#" + method.getMethodName();
  }
  private void writeReportToScreen(File reportFile) {
    List<ITestResult> failedTests = getFailedTests();
    StringBuilder failed = new StringBuilder();
    for (int i = 0; i < failedTests.size(); i++) {
      ITestResult failedTest = failedTests.get(i);
      String fqMethod = getFqMethod(failedTest);
      int numFailures = 1;
      // Peek ahead to see if we had multiple failures for the same method
      // In which case, we list it once with a count of the failures.
      while (((i + 1) < failedTests.size()) &&
              fqMethod.equals(getFqMethod(failedTests.get(i+1)))) {
        numFailures++;
        i++;
      }
      failed.append("  ").append(fqMethod);
      if (numFailures > 1) {
        failed.append(" (x " + numFailures + ")");
      }
      failed.append(EOL);
    }
    if (failed.length() > 0) {
      originalSystemErr.println("The following unit tests failed: ");
      originalSystemErr.println(failed);
      originalSystemErr.println();
      originalSystemErr.println("Include the ant option '-Dtest.failures=true' to rerun only the failed tests.");
    } else {
      originalSystemErr.println("All of the tests passed.");
    }
    originalSystemErr.println();
    originalSystemErr.println("Wrote full test report to:");
    originalSystemErr.println(reportFile.getAbsolutePath());
  }
  public void onTestSuccess(ITestResult tr) {
    super.onTestSuccess(tr);
    addTestResult(tr);
  }
  public void onTestFailure(ITestResult tr) {
    super.onTestFailure(tr);
    addTestResult(tr);
    IClass cls = tr.getTestClass();
    ITestNGMethod method = tr.getMethod();
    String fqMethod = cls.getName() + "#" + method.getMethodName();
    StringBuilder failureInfo = new StringBuilder();
    failureInfo.append("Failed Test:  ").append(fqMethod).append(EOL);
    Object[] parameters = tr.getParameters();
    Throwable cause = tr.getThrowable();
    if (cause != null) {
      failureInfo.append("Failure Cause:  ").append(getTestngLessStack(cause));
    }
    for (int i = 0; (parameters != null) && (i < parameters.length); i++) {
      Object parameter = parameters[i];
      failureInfo.append("parameter[" + i + "]: ").append(parameter).append(EOL);
    }
    failureInfo.append(EOL + EOL);
    originalSystemErr.print(EOL + EOL + EOL + "                 T E S T   F A I L U R E ! ! !" + EOL + EOL);
    originalSystemErr.print(failureInfo);
    originalSystemErr.print(DIVIDER_LINE + EOL + EOL);
    _bufferedTestFailures.append(failureInfo);
  }
  private String getTestngLessStack(Throwable t) {
    StackTraceElement[] elements = t.getStackTrace();
    int lowestOpenDSFrame;
    for (lowestOpenDSFrame = elements.length - 1; lowestOpenDSFrame >= 0; lowestOpenDSFrame--) {
      StackTraceElement element = elements[lowestOpenDSFrame];
      String clsName = element.getClassName();
      if (clsName.startsWith("org.opends.") && !clsName.equals("org.opends.server.SuiteRunner")) {
        break;
      }
    }
    StringBuilder buffer = new StringBuilder();
    buffer.append(t).append(EOL);
    for (int i = 0; i <= lowestOpenDSFrame; i++) {
      buffer.append("    ").append(elements[i]).append(EOL);
    }
    return buffer.toString();
  }
  private final static int PAGE_WIDTH = 80;
  private static String center(String header) {
    StringBuilder buffer = new StringBuilder();
    int indent = (PAGE_WIDTH - header.length()) / 2;
    for (int i = 0; i < indent; i++) {
      buffer.append(" ");
    }
    buffer.append(header);
    return buffer.toString();
  }
  public void onTestSkipped(ITestResult tr) {
    super.onTestSkipped(tr);
    // TODO: do we need to do anything with this?
  }
  public void onTestFailedButWithinSuccessPercentage(ITestResult tr) {
    super.onTestFailedButWithinSuccessPercentage(tr);
    addTestResult(tr);
  }
  private void addTestResult(ITestResult result) {
    getResultsForClass(result.getTestClass()).addTestResult(result);
  }
  private final LinkedHashMap<IClass, TestClassResults> _classResults = new LinkedHashMap<IClass, TestClassResults>();
  private TestClassResults getResultsForClass(IClass cls) {
    TestClassResults results = _classResults.get(cls);
    if (results == null) {
      results = new TestClassResults(cls);
      _classResults.put(cls, results);
    }
    return results;
  }
  synchronized StringBuilder getTimingInfo() {
    StringBuilder timingOutput = new StringBuilder();
    timingOutput.append(center("TESTS RUN BY CLASS")).append(EOL);
    timingOutput.append(center("[method-name total-time (total-invocations)]")).append(EOL + EOL);
    for (TestClassResults results: _classResults.values()) {
      results.getTimingInfo(timingOutput);
    }
    timingOutput.append(EOL + DIVIDER_LINE + DIVIDER_LINE + EOL);
    getSlowestTestsOutput(timingOutput);
    return timingOutput;
  }
  private int countTestMethods() {
    int count = 0;
    for (TestClassResults results: _classResults.values()) {
      count += results._methods.size();
    }
    return count;
  }
  private int countTestsWithStatus(int status) {
    int count = 0;
    for (TestClassResults results: _classResults.values()) {
      count += results._resultCounts[status];
    }
    return count;
  }
  synchronized private List<TestMethodResults> getAllMethodResults() {
    List<TestMethodResults> allResults = new ArrayList<TestMethodResults>();
    for (TestClassResults results: _classResults.values()) {
      allResults.addAll(results.getAllMethodResults());
    }
    return allResults;
  }
  private static final int NUM_SLOWEST_METHODS = 100;
  private void getSlowestTestsOutput(StringBuilder timingOutput) {
    timingOutput.append(center("CLASS SUMMARY SORTED BY DURATION")).append(EOL);
    timingOutput.append(center("[class-name total-time (total-invocations)]")).append(EOL + EOL);
    List<TestClassResults> sortedClasses = getClassesDescendingSortedByDuration();
    for (int i = 0; i < sortedClasses.size(); i++) {
      TestClassResults results = sortedClasses.get(i);
      timingOutput.append("  ");
      results.getSummaryTimingInfo(timingOutput);
      timingOutput.append(EOL);
    }
    timingOutput.append(EOL + DIVIDER_LINE + EOL + EOL);
    timingOutput.append(center("SLOWEST METHODS")).append(EOL);
    timingOutput.append(center("[method-name total-time (total-invocations)]")).append(EOL + EOL);
    List<TestMethodResults> sortedMethods = getMethodsDescendingSortedByDuration();
    for (int i = 0; i < Math.min(sortedMethods.size(), NUM_SLOWEST_METHODS); i++) {
      TestMethodResults results = sortedMethods.get(i);
      results.getTimingInfo(timingOutput, true);
    }
  }
  private List<TestMethodResults> getMethodsDescendingSortedByDuration() {
    List<TestMethodResults> allMethods = getAllMethodResults();
    Collections.sort(allMethods, new Comparator<TestMethodResults>() {
      public int compare(TestMethodResults o1, TestMethodResults o2) {
        if (o1._totalDurationMs > o2._totalDurationMs) {
          return -1;
        } else if (o1._totalDurationMs < o2._totalDurationMs) {
          return 1;
        } else {
          return 0;
        }
      }
    });
    return allMethods;
  }
  private List<TestClassResults> getClassesDescendingSortedByDuration() {
    List<TestClassResults> allClasses = new ArrayList<TestClassResults>(_classResults.values());
    Collections.sort(allClasses, new Comparator<TestClassResults>() {
      public int compare(TestClassResults o1, TestClassResults o2) {
        if (o1._totalDurationMs > o2._totalDurationMs) {
          return -1;
        } else if (o1._totalDurationMs < o2._totalDurationMs) {
          return 1;
        } else {
          return 0;
        }
      }
    });
    return allClasses;
  }
  private final static String[] STATUSES =
          {"<<invalid>>", "Success", "Failure", "Skip", "Success Percentage Failure"};
  /**
   *
   */
  private static class TestClassResults {
    private final IClass _cls;
    private final LinkedHashMap<ITestNGMethod, TestMethodResults> _methods = new LinkedHashMap<ITestNGMethod, TestMethodResults>();
    private int _totalInvocations = 0;
    private long _totalDurationMs = 0;
    // Indexed by SUCCESS, FAILURE, SKIP, SUCCESS_PERCENTAGE_FAILURE
    private int[] _resultCounts = new int[STATUSES.length];
    public TestClassResults(IClass cls) {
      _cls = cls;
    }
    synchronized void addTestResult(ITestResult result) {
      _totalInvocations++;
      _totalDurationMs += result.getEndMillis() - result.getStartMillis();
      getResultsForMethod(result.getMethod()).addTestResult(result);
      int status = result.getStatus();
      if (status < 0 || status >= _resultCounts.length) {
        status = 0;
      }
      _resultCounts[status]++;
    }
    private TestMethodResults getResultsForMethod(ITestNGMethod method) {
      TestMethodResults results = _methods.get(method);
      if (results == null) {
        results = new TestMethodResults(method);
        _methods.put(method, results);
      }
      return results;
    }
    synchronized void getSummaryTimingInfo(StringBuilder timingOutput) {
      timingOutput.append(_cls.getRealClass().getName() + "    ");
      timingOutput.append(getTotalDurationMs() + " ms" + " (" + getTotalInvocations() + ")");
    }
    synchronized Collection<TestMethodResults> getAllMethodResults() {
      return _methods.values();
    }
    synchronized void getTimingInfo(StringBuilder timingOutput) {
      getSummaryTimingInfo(timingOutput);
      timingOutput.append(EOL);
      for (TestMethodResults results: getAllMethodResults()) {
        results.getTimingInfo(timingOutput, false);
      }
      timingOutput.append(EOL);
    }
    int getTotalInvocations() {
      return _totalInvocations;
    }
    long getTotalDurationMs() {
      return _totalDurationMs;
    }
  }
  /**
   *
   */
  private static class TestMethodResults {
    private final ITestNGMethod _method;
    int _totalInvocations = 0;
    long _totalDurationMs = 0;
    // Indexed by SUCCESS, FAILURE, SKIP, SUCCESS_PERCENTAGE_FAILURE
    private int[] _resultCounts = new int[STATUSES.length];
    public TestMethodResults(ITestNGMethod method) {
      _method = method;
    }
    synchronized void addTestResult(ITestResult result) {
      _totalInvocations++;
      _totalDurationMs += result.getEndMillis() - result.getStartMillis();
      int status = result.getStatus();
      if (status < 0 || status >= _resultCounts.length) {
        status = 0;
      }
      _resultCounts[status]++;
    }
    synchronized void getTimingInfo(StringBuilder timingOutput, boolean includeClassName) {
      timingOutput.append("    ");
      if (includeClassName) {
        timingOutput.append(_method.getRealClass().getName()).append("#");
      }
      timingOutput.append(_method.getMethodName() + "  ");
      timingOutput.append(_totalDurationMs + " ms" + " (" + _totalInvocations + ")");
      if (_resultCounts[ITestResult.FAILURE] > 0) {
        timingOutput.append(" " + _resultCounts[ITestResult.FAILURE] + " failure(s)");
      }
      timingOutput.append(EOL);
    }
  }
}