Home | History | Annotate | Download | only in reporters
      1 package org.testng.reporters;
      2 
      3 import java.io.BufferedWriter;
      4 import java.io.File;
      5 import java.io.FileWriter;
      6 import java.io.IOException;
      7 import java.io.PrintWriter;
      8 import java.text.NumberFormat;
      9 import java.util.Collections;
     10 import java.util.Comparator;
     11 import java.util.Iterator;
     12 import java.util.List;
     13 import java.util.Set;
     14 
     15 import org.testng.IReporter;
     16 import org.testng.ISuite;
     17 import org.testng.ISuiteResult;
     18 import org.testng.ITestContext;
     19 import org.testng.ITestResult;
     20 import org.testng.Reporter;
     21 import org.testng.collections.Lists;
     22 import org.testng.internal.Utils;
     23 import org.testng.log4testng.Logger;
     24 import org.testng.xml.XmlSuite;
     25 
     26 /**
     27  * Reporter that generates a single-page HTML report of the test results.
     28  * <p>
     29  * Based on an earlier implementation by Paul Mendelson.
     30  * </p>
     31  *
     32  * @author Abraham Lin
     33  */
     34 public class EmailableReporter2 implements IReporter {
     35     private static final Logger LOG = Logger.getLogger(EmailableReporter.class);
     36 
     37     protected PrintWriter writer;
     38 
     39     protected List<SuiteResult> suiteResults = Lists.newArrayList();
     40 
     41     // Reusable buffer
     42     private StringBuilder buffer = new StringBuilder();
     43 
     44     @Override
     45     public void generateReport(List<XmlSuite> xmlSuites, List<ISuite> suites,
     46             String outputDirectory) {
     47         try {
     48             writer = createWriter(outputDirectory);
     49         } catch (IOException e) {
     50             LOG.error("Unable to create output file", e);
     51             return;
     52         }
     53         for (ISuite suite : suites) {
     54             suiteResults.add(new SuiteResult(suite));
     55         }
     56 
     57         writeDocumentStart();
     58         writeHead();
     59         writeBody();
     60         writeDocumentEnd();
     61 
     62         writer.close();
     63     }
     64 
     65     protected PrintWriter createWriter(String outdir) throws IOException {
     66         new File(outdir).mkdirs();
     67         return new PrintWriter(new BufferedWriter(new FileWriter(new File(
     68                 outdir, "emailable-report.html"))));
     69     }
     70 
     71     protected void writeDocumentStart() {
     72         writer.println("<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.1//EN\" \"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd\">");
     73         writer.print("<html xmlns=\"http://www.w3.org/1999/xhtml\">");
     74     }
     75 
     76     protected void writeHead() {
     77         writer.print("<head>");
     78         writer.print("<title>TestNG Report</title>");
     79         writeStylesheet();
     80         writer.print("</head>");
     81     }
     82 
     83     protected void writeStylesheet() {
     84         writer.print("<style type=\"text/css\">");
     85         writer.print("table {margin-bottom:10px;border-collapse:collapse;empty-cells:show}");
     86         writer.print("th,td {border:1px solid #009;padding:.25em .5em}");
     87         writer.print("th {vertical-align:bottom}");
     88         writer.print("td {vertical-align:top}");
     89         writer.print("table a {font-weight:bold}");
     90         writer.print(".stripe td {background-color: #E6EBF9}");
     91         writer.print(".num {text-align:right}");
     92         writer.print(".passedodd td {background-color: #3F3}");
     93         writer.print(".passedeven td {background-color: #0A0}");
     94         writer.print(".skippedodd td {background-color: #DDD}");
     95         writer.print(".skippedeven td {background-color: #CCC}");
     96         writer.print(".failedodd td,.attn {background-color: #F33}");
     97         writer.print(".failedeven td,.stripe .attn {background-color: #D00}");
     98         writer.print(".stacktrace {white-space:pre;font-family:monospace}");
     99         writer.print(".totop {font-size:85%;text-align:center;border-bottom:2px solid #000}");
    100         writer.print("</style>");
    101     }
    102 
    103     protected void writeBody() {
    104         writer.print("<body>");
    105         writeSuiteSummary();
    106         writeScenarioSummary();
    107         writeScenarioDetails();
    108         writer.print("</body>");
    109     }
    110 
    111     protected void writeDocumentEnd() {
    112         writer.print("</html>");
    113     }
    114 
    115     protected void writeSuiteSummary() {
    116         NumberFormat integerFormat = NumberFormat.getIntegerInstance();
    117         NumberFormat decimalFormat = NumberFormat.getNumberInstance();
    118 
    119         int totalPassedTests = 0;
    120         int totalSkippedTests = 0;
    121         int totalFailedTests = 0;
    122         long totalDuration = 0;
    123 
    124         writer.print("<table>");
    125         writer.print("<tr>");
    126         writer.print("<th>Test</th>");
    127         writer.print("<th># Passed</th>");
    128         writer.print("<th># Skipped</th>");
    129         writer.print("<th># Failed</th>");
    130         writer.print("<th>Time (ms)</th>");
    131         writer.print("<th>Included Groups</th>");
    132         writer.print("<th>Excluded Groups</th>");
    133         writer.print("</tr>");
    134 
    135         int testIndex = 0;
    136         for (SuiteResult suiteResult : suiteResults) {
    137             writer.print("<tr><th colspan=\"7\">");
    138             writer.print(Utils.escapeHtml(suiteResult.getSuiteName()));
    139             writer.print("</th></tr>");
    140 
    141             for (TestResult testResult : suiteResult.getTestResults()) {
    142                 int passedTests = testResult.getPassedTestCount();
    143                 int skippedTests = testResult.getSkippedTestCount();
    144                 int failedTests = testResult.getFailedTestCount();
    145                 long duration = testResult.getDuration();
    146 
    147                 writer.print("<tr");
    148                 if ((testIndex % 2) == 1) {
    149                     writer.print(" class=\"stripe\"");
    150                 }
    151                 writer.print(">");
    152 
    153                 buffer.setLength(0);
    154                 writeTableData(buffer.append("<a href=\"#t").append(testIndex)
    155                         .append("\">")
    156                         .append(Utils.escapeHtml(testResult.getTestName()))
    157                         .append("</a>").toString());
    158                 writeTableData(integerFormat.format(passedTests), "num");
    159                 writeTableData(integerFormat.format(skippedTests),
    160                         (skippedTests > 0 ? "num attn" : "num"));
    161                 writeTableData(integerFormat.format(failedTests),
    162                         (failedTests > 0 ? "num attn" : "num"));
    163                 writeTableData(decimalFormat.format(duration), "num");
    164                 writeTableData(testResult.getIncludedGroups());
    165                 writeTableData(testResult.getExcludedGroups());
    166 
    167                 writer.print("</tr>");
    168 
    169                 totalPassedTests += passedTests;
    170                 totalSkippedTests += skippedTests;
    171                 totalFailedTests += failedTests;
    172                 totalDuration += duration;
    173 
    174                 testIndex++;
    175             }
    176         }
    177 
    178         // Print totals if there was more than one test
    179         if (testIndex > 1) {
    180             writer.print("<tr>");
    181             writer.print("<th>Total</th>");
    182             writeTableHeader(integerFormat.format(totalPassedTests), "num");
    183             writeTableHeader(integerFormat.format(totalSkippedTests),
    184                     (totalSkippedTests > 0 ? "num attn" : "num"));
    185             writeTableHeader(integerFormat.format(totalFailedTests),
    186                     (totalFailedTests > 0 ? "num attn" : "num"));
    187             writeTableHeader(decimalFormat.format(totalDuration), "num");
    188             writer.print("<th colspan=\"2\"></th>");
    189             writer.print("</tr>");
    190         }
    191 
    192         writer.print("</table>");
    193     }
    194 
    195     /**
    196      * Writes a summary of all the test scenarios.
    197      */
    198     protected void writeScenarioSummary() {
    199         writer.print("<table>");
    200         writer.print("<thead>");
    201         writer.print("<tr>");
    202         writer.print("<th>Class</th>");
    203         writer.print("<th>Method</th>");
    204         writer.print("<th>Start</th>");
    205         writer.print("<th>Time (ms)</th>");
    206         writer.print("</tr>");
    207         writer.print("</thead>");
    208 
    209         int testIndex = 0;
    210         int scenarioIndex = 0;
    211         for (SuiteResult suiteResult : suiteResults) {
    212             writer.print("<tbody><tr><th colspan=\"4\">");
    213             writer.print(Utils.escapeHtml(suiteResult.getSuiteName()));
    214             writer.print("</th></tr></tbody>");
    215 
    216             for (TestResult testResult : suiteResult.getTestResults()) {
    217                 writer.print("<tbody id=\"t");
    218                 writer.print(testIndex);
    219                 writer.print("\">");
    220 
    221                 String testName = Utils.escapeHtml(testResult.getTestName());
    222 
    223                 scenarioIndex += writeScenarioSummary(testName
    224                         + " &#8212; failed (configuration methods)",
    225                         testResult.getFailedConfigurationResults(), "failed",
    226                         scenarioIndex);
    227                 scenarioIndex += writeScenarioSummary(testName
    228                         + " &#8212; failed", testResult.getFailedTestResults(),
    229                         "failed", scenarioIndex);
    230                 scenarioIndex += writeScenarioSummary(testName
    231                         + " &#8212; skipped (configuration methods)",
    232                         testResult.getSkippedConfigurationResults(), "skipped",
    233                         scenarioIndex);
    234                 scenarioIndex += writeScenarioSummary(testName
    235                         + " &#8212; skipped",
    236                         testResult.getSkippedTestResults(), "skipped",
    237                         scenarioIndex);
    238                 scenarioIndex += writeScenarioSummary(testName
    239                         + " &#8212; passed", testResult.getPassedTestResults(),
    240                         "passed", scenarioIndex);
    241 
    242                 writer.print("</tbody>");
    243 
    244                 testIndex++;
    245             }
    246         }
    247 
    248         writer.print("</table>");
    249     }
    250 
    251     /**
    252      * Writes the scenario summary for the results of a given state for a single
    253      * test.
    254      */
    255     private int writeScenarioSummary(String description,
    256             List<ClassResult> classResults, String cssClassPrefix,
    257             int startingScenarioIndex) {
    258         int scenarioCount = 0;
    259         if (!classResults.isEmpty()) {
    260             writer.print("<tr><th colspan=\"4\">");
    261             writer.print(description);
    262             writer.print("</th></tr>");
    263 
    264             int scenarioIndex = startingScenarioIndex;
    265             int classIndex = 0;
    266             for (ClassResult classResult : classResults) {
    267                 String cssClass = cssClassPrefix
    268                         + ((classIndex % 2) == 0 ? "even" : "odd");
    269 
    270                 buffer.setLength(0);
    271 
    272                 int scenariosPerClass = 0;
    273                 int methodIndex = 0;
    274                 for (MethodResult methodResult : classResult.getMethodResults()) {
    275                     List<ITestResult> results = methodResult.getResults();
    276                     int resultsCount = results.size();
    277                     assert resultsCount > 0;
    278 
    279                     ITestResult firstResult = results.iterator().next();
    280                     String methodName = Utils.escapeHtml(firstResult
    281                             .getMethod().getMethodName());
    282                     long start = firstResult.getStartMillis();
    283                     long duration = firstResult.getEndMillis() - start;
    284 
    285                     // The first method per class shares a row with the class
    286                     // header
    287                     if (methodIndex > 0) {
    288                         buffer.append("<tr class=\"").append(cssClass)
    289                                 .append("\">");
    290 
    291                     }
    292 
    293                     // Write the timing information with the first scenario per
    294                     // method
    295                     buffer.append("<td><a href=\"#m").append(scenarioIndex)
    296                             .append("\">").append(methodName)
    297                             .append("</a></td>").append("<td rowspan=\"")
    298                             .append(resultsCount).append("\">").append(start)
    299                             .append("</td>").append("<td rowspan=\"")
    300                             .append(resultsCount).append("\">")
    301                             .append(duration).append("</td></tr>");
    302                     scenarioIndex++;
    303 
    304                     // Write the remaining scenarios for the method
    305                     for (int i = 1; i < resultsCount; i++) {
    306                         buffer.append("<tr class=\"").append(cssClass)
    307                                 .append("\">").append("<td><a href=\"#m")
    308                                 .append(scenarioIndex).append("\">")
    309                                 .append(methodName).append("</a></td></tr>");
    310                         scenarioIndex++;
    311                     }
    312 
    313                     scenariosPerClass += resultsCount;
    314                     methodIndex++;
    315                 }
    316 
    317                 // Write the test results for the class
    318                 writer.print("<tr class=\"");
    319                 writer.print(cssClass);
    320                 writer.print("\">");
    321                 writer.print("<td rowspan=\"");
    322                 writer.print(scenariosPerClass);
    323                 writer.print("\">");
    324                 writer.print(Utils.escapeHtml(classResult.getClassName()));
    325                 writer.print("</td>");
    326                 writer.print(buffer);
    327 
    328                 classIndex++;
    329             }
    330             scenarioCount = scenarioIndex - startingScenarioIndex;
    331         }
    332         return scenarioCount;
    333     }
    334 
    335     /**
    336      * Writes the details for all test scenarios.
    337      */
    338     protected void writeScenarioDetails() {
    339         int scenarioIndex = 0;
    340         for (SuiteResult suiteResult : suiteResults) {
    341             for (TestResult testResult : suiteResult.getTestResults()) {
    342                 writer.print("<h2>");
    343                 writer.print(Utils.escapeHtml(testResult.getTestName()));
    344                 writer.print("</h2>");
    345 
    346                 scenarioIndex += writeScenarioDetails(
    347                         testResult.getFailedConfigurationResults(),
    348                         scenarioIndex);
    349                 scenarioIndex += writeScenarioDetails(
    350                         testResult.getFailedTestResults(), scenarioIndex);
    351                 scenarioIndex += writeScenarioDetails(
    352                         testResult.getSkippedConfigurationResults(),
    353                         scenarioIndex);
    354                 scenarioIndex += writeScenarioDetails(
    355                         testResult.getSkippedTestResults(), scenarioIndex);
    356                 scenarioIndex += writeScenarioDetails(
    357                         testResult.getPassedTestResults(), scenarioIndex);
    358             }
    359         }
    360     }
    361 
    362     /**
    363      * Writes the scenario details for the results of a given state for a single
    364      * test.
    365      */
    366     private int writeScenarioDetails(List<ClassResult> classResults,
    367             int startingScenarioIndex) {
    368         int scenarioIndex = startingScenarioIndex;
    369         for (ClassResult classResult : classResults) {
    370             String className = classResult.getClassName();
    371             for (MethodResult methodResult : classResult.getMethodResults()) {
    372                 List<ITestResult> results = methodResult.getResults();
    373                 assert !results.isEmpty();
    374 
    375                 String label = Utils
    376                         .escapeHtml(className
    377                                 + "#"
    378                                 + results.iterator().next().getMethod()
    379                                         .getMethodName());
    380                 for (ITestResult result : results) {
    381                     writeScenario(scenarioIndex, label, result);
    382                     scenarioIndex++;
    383                 }
    384             }
    385         }
    386 
    387         return scenarioIndex - startingScenarioIndex;
    388     }
    389 
    390     /**
    391      * Writes the details for an individual test scenario.
    392      */
    393     private void writeScenario(int scenarioIndex, String label,
    394             ITestResult result) {
    395         writer.print("<h3 id=\"m");
    396         writer.print(scenarioIndex);
    397         writer.print("\">");
    398         writer.print(label);
    399         writer.print("</h3>");
    400 
    401         writer.print("<table class=\"result\">");
    402 
    403         // Write test parameters (if any)
    404         Object[] parameters = result.getParameters();
    405         int parameterCount = (parameters == null ? 0 : parameters.length);
    406         if (parameterCount > 0) {
    407             writer.print("<tr class=\"param\">");
    408             for (int i = 1; i <= parameterCount; i++) {
    409                 writer.print("<th>Parameter #");
    410                 writer.print(i);
    411                 writer.print("</th>");
    412             }
    413             writer.print("</tr><tr class=\"param stripe\">");
    414             for (Object parameter : parameters) {
    415                 writer.print("<td>");
    416                 writer.print(Utils.escapeHtml(Utils.toString(parameter)));
    417                 writer.print("</td>");
    418             }
    419             writer.print("</tr>");
    420         }
    421 
    422         // Write reporter messages (if any)
    423         List<String> reporterMessages = Reporter.getOutput(result);
    424         if (!reporterMessages.isEmpty()) {
    425             writer.print("<tr><th");
    426             if (parameterCount > 1) {
    427                 writer.print(" colspan=\"");
    428                 writer.print(parameterCount);
    429                 writer.print("\"");
    430             }
    431             writer.print(">Messages</th></tr>");
    432 
    433             writer.print("<tr><td");
    434             if (parameterCount > 1) {
    435                 writer.print(" colspan=\"");
    436                 writer.print(parameterCount);
    437                 writer.print("\"");
    438             }
    439             writer.print(">");
    440             writeReporterMessages(reporterMessages);
    441             writer.print("</td></tr>");
    442         }
    443 
    444         // Write exception (if any)
    445         Throwable throwable = result.getThrowable();
    446         if (throwable != null) {
    447             writer.print("<tr><th");
    448             if (parameterCount > 1) {
    449                 writer.print(" colspan=\"");
    450                 writer.print(parameterCount);
    451                 writer.print("\"");
    452             }
    453             writer.print(">");
    454             writer.print((result.getStatus() == ITestResult.SUCCESS ? "Expected Exception"
    455                     : "Exception"));
    456             writer.print("</th></tr>");
    457 
    458             writer.print("<tr><td");
    459             if (parameterCount > 1) {
    460                 writer.print(" colspan=\"");
    461                 writer.print(parameterCount);
    462                 writer.print("\"");
    463             }
    464             writer.print(">");
    465             writeStackTrace(throwable);
    466             writer.print("</td></tr>");
    467         }
    468 
    469         writer.print("</table>");
    470         writer.print("<p class=\"totop\"><a href=\"#summary\">back to summary</a></p>");
    471     }
    472 
    473     protected void writeReporterMessages(List<String> reporterMessages) {
    474         writer.print("<div class=\"messages\">");
    475         Iterator<String> iterator = reporterMessages.iterator();
    476         assert iterator.hasNext();
    477         writer.print(Utils.escapeHtml(iterator.next()));
    478         while (iterator.hasNext()) {
    479             writer.print("<br/>");
    480             writer.print(Utils.escapeHtml(iterator.next()));
    481         }
    482         writer.print("</div>");
    483     }
    484 
    485     protected void writeStackTrace(Throwable throwable) {
    486         writer.print("<div class=\"stacktrace\">");
    487         writer.print(Utils.stackTrace(throwable, true)[0]);
    488         writer.print("</div>");
    489     }
    490 
    491     /**
    492      * Writes a TH element with the specified contents and CSS class names.
    493      *
    494      * @param html
    495      *            the HTML contents
    496      * @param cssClasses
    497      *            the space-delimited CSS classes or null if there are no
    498      *            classes to apply
    499      */
    500     protected void writeTableHeader(String html, String cssClasses) {
    501         writeTag("th", html, cssClasses);
    502     }
    503 
    504     /**
    505      * Writes a TD element with the specified contents.
    506      *
    507      * @param html
    508      *            the HTML contents
    509      */
    510     protected void writeTableData(String html) {
    511         writeTableData(html, null);
    512     }
    513 
    514     /**
    515      * Writes a TD element with the specified contents and CSS class names.
    516      *
    517      * @param html
    518      *            the HTML contents
    519      * @param cssClasses
    520      *            the space-delimited CSS classes or null if there are no
    521      *            classes to apply
    522      */
    523     protected void writeTableData(String html, String cssClasses) {
    524         writeTag("td", html, cssClasses);
    525     }
    526 
    527     /**
    528      * Writes an arbitrary HTML element with the specified contents and CSS
    529      * class names.
    530      *
    531      * @param tag
    532      *            the tag name
    533      * @param html
    534      *            the HTML contents
    535      * @param cssClasses
    536      *            the space-delimited CSS classes or null if there are no
    537      *            classes to apply
    538      */
    539     protected void writeTag(String tag, String html, String cssClasses) {
    540         writer.print("<");
    541         writer.print(tag);
    542         if (cssClasses != null) {
    543             writer.print(" class=\"");
    544             writer.print(cssClasses);
    545             writer.print("\"");
    546         }
    547         writer.print(">");
    548         writer.print(html);
    549         writer.print("</");
    550         writer.print(tag);
    551         writer.print(">");
    552     }
    553 
    554     /**
    555      * Groups {@link TestResult}s by suite.
    556      */
    557     protected static class SuiteResult {
    558         private final String suiteName;
    559         private final List<TestResult> testResults = Lists.newArrayList();
    560 
    561         public SuiteResult(ISuite suite) {
    562             suiteName = suite.getName();
    563             for (ISuiteResult suiteResult : suite.getResults().values()) {
    564                 testResults.add(new TestResult(suiteResult.getTestContext()));
    565             }
    566         }
    567 
    568         public String getSuiteName() {
    569             return suiteName;
    570         }
    571 
    572         /**
    573          * @return the test results (possibly empty)
    574          */
    575         public List<TestResult> getTestResults() {
    576             return testResults;
    577         }
    578     }
    579 
    580     /**
    581      * Groups {@link ClassResult}s by test, type (configuration or test), and
    582      * status.
    583      */
    584     protected static class TestResult {
    585         /**
    586          * Orders test results by class name and then by method name (in
    587          * lexicographic order).
    588          */
    589         protected static final Comparator<ITestResult> RESULT_COMPARATOR = new Comparator<ITestResult>() {
    590             @Override
    591             public int compare(ITestResult o1, ITestResult o2) {
    592                 int result = o1.getTestClass().getName()
    593                         .compareTo(o2.getTestClass().getName());
    594                 if (result == 0) {
    595                     result = o1.getMethod().getMethodName()
    596                             .compareTo(o2.getMethod().getMethodName());
    597                 }
    598                 return result;
    599             }
    600         };
    601 
    602         private final String testName;
    603         private final List<ClassResult> failedConfigurationResults;
    604         private final List<ClassResult> failedTestResults;
    605         private final List<ClassResult> skippedConfigurationResults;
    606         private final List<ClassResult> skippedTestResults;
    607         private final List<ClassResult> passedTestResults;
    608         private final int failedTestCount;
    609         private final int skippedTestCount;
    610         private final int passedTestCount;
    611         private final long duration;
    612         private final String includedGroups;
    613         private final String excludedGroups;
    614 
    615         public TestResult(ITestContext context) {
    616             testName = context.getName();
    617 
    618             Set<ITestResult> failedConfigurations = context
    619                     .getFailedConfigurations().getAllResults();
    620             Set<ITestResult> failedTests = context.getFailedTests()
    621                     .getAllResults();
    622             Set<ITestResult> skippedConfigurations = context
    623                     .getSkippedConfigurations().getAllResults();
    624             Set<ITestResult> skippedTests = context.getSkippedTests()
    625                     .getAllResults();
    626             Set<ITestResult> passedTests = context.getPassedTests()
    627                     .getAllResults();
    628 
    629             failedConfigurationResults = groupResults(failedConfigurations);
    630             failedTestResults = groupResults(failedTests);
    631             skippedConfigurationResults = groupResults(skippedConfigurations);
    632             skippedTestResults = groupResults(skippedTests);
    633             passedTestResults = groupResults(passedTests);
    634 
    635             failedTestCount = failedTests.size();
    636             skippedTestCount = skippedTests.size();
    637             passedTestCount = passedTests.size();
    638 
    639             duration = context.getEndDate().getTime()
    640                     - context.getStartDate().getTime();
    641 
    642             includedGroups = formatGroups(context.getIncludedGroups());
    643             excludedGroups = formatGroups(context.getExcludedGroups());
    644         }
    645 
    646         /**
    647          * Groups test results by method and then by class.
    648          */
    649         protected List<ClassResult> groupResults(Set<ITestResult> results) {
    650             List<ClassResult> classResults = Lists.newArrayList();
    651             if (!results.isEmpty()) {
    652                 List<MethodResult> resultsPerClass = Lists.newArrayList();
    653                 List<ITestResult> resultsPerMethod = Lists.newArrayList();
    654 
    655                 List<ITestResult> resultsList = Lists.newArrayList(results);
    656                 Collections.sort(resultsList, RESULT_COMPARATOR);
    657                 Iterator<ITestResult> resultsIterator = resultsList.iterator();
    658                 assert resultsIterator.hasNext();
    659 
    660                 ITestResult result = resultsIterator.next();
    661                 resultsPerMethod.add(result);
    662 
    663                 String previousClassName = result.getTestClass().getName();
    664                 String previousMethodName = result.getMethod().getMethodName();
    665                 while (resultsIterator.hasNext()) {
    666                     result = resultsIterator.next();
    667 
    668                     String className = result.getTestClass().getName();
    669                     if (!previousClassName.equals(className)) {
    670                         // Different class implies different method
    671                         assert !resultsPerMethod.isEmpty();
    672                         resultsPerClass.add(new MethodResult(resultsPerMethod));
    673                         resultsPerMethod = Lists.newArrayList();
    674 
    675                         assert !resultsPerClass.isEmpty();
    676                         classResults.add(new ClassResult(previousClassName,
    677                                 resultsPerClass));
    678                         resultsPerClass = Lists.newArrayList();
    679 
    680                         previousClassName = className;
    681                         previousMethodName = result.getMethod().getMethodName();
    682                     } else {
    683                         String methodName = result.getMethod().getMethodName();
    684                         if (!previousMethodName.equals(methodName)) {
    685                             assert !resultsPerMethod.isEmpty();
    686                             resultsPerClass.add(new MethodResult(resultsPerMethod));
    687                             resultsPerMethod = Lists.newArrayList();
    688 
    689                             previousMethodName = methodName;
    690                         }
    691                     }
    692                     resultsPerMethod.add(result);
    693                 }
    694                 assert !resultsPerMethod.isEmpty();
    695                 resultsPerClass.add(new MethodResult(resultsPerMethod));
    696                 assert !resultsPerClass.isEmpty();
    697                 classResults.add(new ClassResult(previousClassName,
    698                         resultsPerClass));
    699             }
    700             return classResults;
    701         }
    702 
    703         public String getTestName() {
    704             return testName;
    705         }
    706 
    707         /**
    708          * @return the results for failed configurations (possibly empty)
    709          */
    710         public List<ClassResult> getFailedConfigurationResults() {
    711             return failedConfigurationResults;
    712         }
    713 
    714         /**
    715          * @return the results for failed tests (possibly empty)
    716          */
    717         public List<ClassResult> getFailedTestResults() {
    718             return failedTestResults;
    719         }
    720 
    721         /**
    722          * @return the results for skipped configurations (possibly empty)
    723          */
    724         public List<ClassResult> getSkippedConfigurationResults() {
    725             return skippedConfigurationResults;
    726         }
    727 
    728         /**
    729          * @return the results for skipped tests (possibly empty)
    730          */
    731         public List<ClassResult> getSkippedTestResults() {
    732             return skippedTestResults;
    733         }
    734 
    735         /**
    736          * @return the results for passed tests (possibly empty)
    737          */
    738         public List<ClassResult> getPassedTestResults() {
    739             return passedTestResults;
    740         }
    741 
    742         public int getFailedTestCount() {
    743             return failedTestCount;
    744         }
    745 
    746         public int getSkippedTestCount() {
    747             return skippedTestCount;
    748         }
    749 
    750         public int getPassedTestCount() {
    751             return passedTestCount;
    752         }
    753 
    754         public long getDuration() {
    755             return duration;
    756         }
    757 
    758         public String getIncludedGroups() {
    759             return includedGroups;
    760         }
    761 
    762         public String getExcludedGroups() {
    763             return excludedGroups;
    764         }
    765 
    766         /**
    767          * Formats an array of groups for display.
    768          */
    769         protected String formatGroups(String[] groups) {
    770             if (groups.length == 0) {
    771                 return "";
    772             }
    773 
    774             StringBuilder builder = new StringBuilder();
    775             builder.append(groups[0]);
    776             for (int i = 1; i < groups.length; i++) {
    777                 builder.append(", ").append(groups[i]);
    778             }
    779             return builder.toString();
    780         }
    781     }
    782 
    783     /**
    784      * Groups {@link MethodResult}s by class.
    785      */
    786     protected static class ClassResult {
    787         private final String className;
    788         private final List<MethodResult> methodResults;
    789 
    790         /**
    791          * @param className
    792          *            the class name
    793          * @param methodResults
    794          *            the non-null, non-empty {@link MethodResult} list
    795          */
    796         public ClassResult(String className, List<MethodResult> methodResults) {
    797             this.className = className;
    798             this.methodResults = methodResults;
    799         }
    800 
    801         public String getClassName() {
    802             return className;
    803         }
    804 
    805         /**
    806          * @return the non-null, non-empty {@link MethodResult} list
    807          */
    808         public List<MethodResult> getMethodResults() {
    809             return methodResults;
    810         }
    811     }
    812 
    813     /**
    814      * Groups test results by method.
    815      */
    816     protected static class MethodResult {
    817         private final List<ITestResult> results;
    818 
    819         /**
    820          * @param results
    821          *            the non-null, non-empty result list
    822          */
    823         public MethodResult(List<ITestResult> results) {
    824             this.results = results;
    825         }
    826 
    827         /**
    828          * @return the non-null, non-empty result list
    829          */
    830         public List<ITestResult> getResults() {
    831             return results;
    832         }
    833     }
    834 }
    835