1 package org.testng.reporters;
2 
3 import static org.testng.internal.Utils.isStringNotEmpty;
4 
5 import org.testng.IInvokedMethod;
6 import org.testng.IReporter;
7 import org.testng.ISuite;
8 import org.testng.ISuiteResult;
9 import org.testng.ITestClass;
10 import org.testng.ITestContext;
11 import org.testng.ITestNGMethod;
12 import org.testng.Reporter;
13 import org.testng.collections.Maps;
14 import org.testng.internal.Utils;
15 import org.testng.xml.XmlSuite;
16 
17 import java.io.BufferedWriter;
18 import java.io.File;
19 import java.io.IOException;
20 import java.lang.reflect.Method;
21 import java.text.SimpleDateFormat;
22 import java.util.Arrays;
23 import java.util.Collection;
24 import java.util.Collections;
25 import java.util.Comparator;
26 import java.util.List;
27 import java.util.Map;
28 
29 /**
30  * This class implements an HTML reporter for suites.
31  *
32  * @author cbeust
33  * @author <a href='mailto:the_mindstorm@evolva.ro'>Alexandru Popescu</a>
34  */
35 public class SuiteHTMLReporter implements IReporter {
36   public static final String METHODS_CHRONOLOGICAL = "methods.html";
37   public static final String METHODS_ALPHABETICAL = "methods-alphabetical.html";
38   public static final String GROUPS = "groups.html";
39   public static final String CLASSES = "classes.html";
40   public static final String REPORTER_OUTPUT = "reporter-output.html";
41   public static final String METHODS_NOT_RUN = "methods-not-run.html";
42   public static final String TESTNG_XML = "testng.xml.html";
43 
44   private Map<String, ITestClass> m_classes = Maps.newHashMap();
45   private String m_outputDirectory;
46 
47   @Override
generateReport(List<XmlSuite> xmlSuites, List<ISuite> suites, String outputDirectory)48   public void generateReport(List<XmlSuite> xmlSuites, List<ISuite> suites, String outputDirectory) {
49     m_outputDirectory = generateOutputDirectoryName(outputDirectory + File.separator + "old");
50 
51     try {
52       HtmlHelper.generateStylesheet(outputDirectory);
53     } catch (IOException e) {
54       //  TODO Propagate the exception properly.
55       e.printStackTrace();
56     }
57 
58     for (ISuite suite : suites) {
59 
60       //
61       // Generate the various reports
62       //
63       XmlSuite xmlSuite = suite.getXmlSuite();
64       if (xmlSuite.getTests().size() == 0) {
65         continue;
66       }
67       generateTableOfContents(xmlSuite, suite);
68       generateSuites(xmlSuite, suite);
69       generateIndex(xmlSuite, suite);
70       generateMain(xmlSuite, suite);
71       generateMethodsAndGroups(xmlSuite, suite);
72       generateMethodsChronologically(xmlSuite, suite, METHODS_CHRONOLOGICAL, false);
73       generateMethodsChronologically(xmlSuite, suite, METHODS_ALPHABETICAL, true);
74       generateClasses(xmlSuite, suite);
75       generateReporterOutput(xmlSuite, suite);
76       generateExcludedMethodsReport(xmlSuite, suite);
77       generateXmlFile(xmlSuite, suite);
78     }
79 
80     generateIndex(suites);
81   }
82 
83   /**
84    * Overridable by subclasses to create different directory names (e.g. with timestamps).
85    * @param outputDirectory the output directory specified by the user
86    */
generateOutputDirectoryName(String outputDirectory)87   protected String generateOutputDirectoryName(String outputDirectory) {
88     return outputDirectory;
89   }
90 
generateXmlFile(XmlSuite xmlSuite, ISuite suite)91   private void generateXmlFile(XmlSuite xmlSuite, ISuite suite) {
92     String content = xmlSuite.toXml().replaceAll("<", "&lt;").replaceAll(">", "&gt;")
93           .replaceAll(" ", "&nbsp;").replaceAll("\n", "<br/>");
94 
95     StringBuffer sb = new StringBuffer("<html>");
96 
97     sb.append("<head><title>").append("testng.xml for ")
98       .append(xmlSuite.getName()).append("</title></head><body><tt>")
99       .append(content)
100       .append("</tt></body></html>");
101 
102     Utils.writeFile(getOutputDirectory(xmlSuite), TESTNG_XML, sb.toString());
103   }
104 
105   /**
106    * Generate the main index.html file that lists all the suites
107    * and their result
108    */
generateIndex(List<ISuite> suites)109   private void generateIndex(List<ISuite> suites) {
110     StringBuffer sb = new StringBuffer();
111     String title = "Test results";
112     sb.append("<html>\n<head><title>" + title + "</title>")
113       .append(HtmlHelper.getCssString("."))
114       .append("</head><body>\n")
115       .append("<h2><p align='center'>").append(title).append("</p></h2>\n")
116       .append("<table border='1' width='100%' class='main-page'>")
117       .append("<tr><th>Suite</th><th>Passed</th><th>Failed</th><th>Skipped</th><th>testng.xml</th></tr>\n");
118 
119     int totalFailedTests = 0;
120     int totalPassedTests = 0;
121     int totalSkippedTests = 0;
122 
123     StringBuffer suiteBuf= new StringBuffer();
124     for (ISuite suite : suites) {
125       if (suite.getResults().size() == 0) {
126         continue;
127       }
128 
129       String name = suite.getName();
130 
131       int failedTests= 0;
132       int passedTests= 0;
133       int skippedTests= 0;
134 
135       Map<String, ISuiteResult> results = suite.getResults();
136       for (ISuiteResult result : results.values()) {
137         ITestContext context = result.getTestContext();
138         failedTests += context.getFailedTests().size();
139         totalFailedTests += context.getFailedTests().size();
140         passedTests += context.getPassedTests().size();
141         totalPassedTests += context.getPassedTests().size();
142         skippedTests += context.getSkippedTests().size();
143         totalSkippedTests += context.getSkippedTests().size();
144       }
145 
146       String cls = failedTests > 0 ? "invocation-failed"
147           : (passedTests > 0  ? "invocation-passed" : "invocation-failed");
148       suiteBuf.append("<tr align='center' class='").append(cls).append("'>")
149         .append("<td><a href='").append(name).append("/index.html'>")
150         .append(name).append("</a></td>\n");
151       suiteBuf.append("<td>" + passedTests + "</td>")
152         .append("<td>" + failedTests + "</td>")
153         .append("<td>" + skippedTests + "</td>")
154         .append("<td><a href='").append(name).append("/").append(TESTNG_XML).append("'>Link").append("</a></td>")
155         .append("</tr>");
156 
157     }
158 
159     String cls= totalFailedTests > 0 ? "invocation-failed"
160         : (totalPassedTests > 0 ? "invocation-passed" : "invocation-failed");
161     sb.append("<tr align='center' class='").append(cls).append("'>")
162       .append("<td><em>Total</em></td>")
163       .append("<td><em>").append(totalPassedTests).append("</em></td>")
164       .append("<td><em>").append(totalFailedTests).append("</em></td>")
165       .append("<td><em>").append(totalSkippedTests).append("</em></td>")
166       .append("<td>&nbsp;</td>")
167       .append("</tr>\n");
168     sb.append(suiteBuf);
169     sb.append("</table>").append("</body></html>\n");
170 
171     Utils.writeFile(m_outputDirectory, "index.html", sb.toString());
172   }
173 
generateExcludedMethodsReport(XmlSuite xmlSuite, ISuite suite)174   private void generateExcludedMethodsReport(XmlSuite xmlSuite, ISuite suite) {
175       Collection<ITestNGMethod> excluded = suite.getExcludedMethods();
176       StringBuffer sb2 = new StringBuffer("<h2>Methods that were not run</h2><table>\n");
177       for (ITestNGMethod method : excluded) {
178         Method m = method.getMethod();
179         if (m != null) {
180           sb2.append("<tr><td>")
181           .append(m.getDeclaringClass().getName() + "." + m.getName());
182           String description = method.getDescription();
183           if(isStringNotEmpty(description)) {
184             sb2.append("<br/>").append(SP2).append("<i>").append(description).append("</i>");
185           }
186           sb2.append("</td></tr>\n");
187         }
188       }
189       sb2.append("</table>");
190 
191       Utils.writeFile(getOutputDirectory(xmlSuite), METHODS_NOT_RUN, sb2.toString());
192   }
193 
generateReporterOutput(XmlSuite xmlSuite, ISuite suite)194   private void generateReporterOutput(XmlSuite xmlSuite, ISuite suite) {
195     StringBuffer sb = new StringBuffer();
196 
197     //
198     // Reporter output
199     //
200     sb.append("<h2>Reporter output</h2>")
201       .append("<table>");
202     List<String> output = Reporter.getOutput();
203     for (String line : output) {
204       sb.append("<tr><td>").append(line).append("</td></tr>\n");
205     }
206 
207     sb.append("</table>");
208 
209     Utils.writeFile(getOutputDirectory(xmlSuite), REPORTER_OUTPUT, sb.toString());
210   }
211 
generateClasses(XmlSuite xmlSuite, ISuite suite)212   private void generateClasses(XmlSuite xmlSuite, ISuite suite) {
213     StringBuffer sb = new StringBuffer();
214     sb.append("<table border='1'>\n")
215     .append("<tr>\n")
216     .append("<th>Class name</th>\n")
217     .append("<th>Method name</th>\n")
218     .append("<th>Groups</th>\n")
219     .append("</tr>")
220     ;
221     for (ITestClass tc : m_classes.values()) {
222       sb.append(generateClass(tc));
223     }
224 
225     sb.append("</table>\n");
226 
227     Utils.writeFile(getOutputDirectory(xmlSuite), CLASSES, sb.toString());
228   }
229 
230   private final static String SP = "&nbsp;";
231   private final static String SP2 = SP + SP + SP + SP;
232   private final static String SP3 = SP2 + SP2;
233   private final static String SP4 = SP3 + SP3;
234 
generateClass(ITestClass cls)235   private String generateClass(ITestClass cls) {
236     StringBuffer sb = new StringBuffer();
237 
238     sb.append("<tr>\n")
239       .append("<td>").append(cls.getRealClass().getName()).append("</td>\n")
240       .append("<td>&nbsp;</td>")
241       .append("<td>&nbsp;</td>")
242       .append("</tr>\n")
243       ;
244 
245     String[] tags = new String[] {
246         "@Test",
247         "@BeforeClass",
248         "@BeforeMethod",
249         "@AfterMethod",
250         "@AfterClass"
251     };
252     ITestNGMethod[][] methods = new ITestNGMethod[][] {
253       cls.getTestMethods(),
254       cls.getBeforeClassMethods(),
255       cls.getBeforeTestMethods(),
256       cls.getAfterTestMethods(),
257       cls.getAfterClassMethods()
258     };
259 
260     for (int i = 0; i < tags.length; i++) {
261       sb.append("<tr>\n")
262       .append("<td align='center' colspan='3'>").append(tags[i]).append("</td>\n")
263       .append("</tr>\n")
264       .append(dumpMethods(methods[i]))
265       ;
266     }
267 //    sb.append("<hr width='100%'/>")
268 //    .append("<h3>").append(cls.getRealClass().getName()).append("</h3>\n");
269 //
270 //    sb.append("<div>").append(SP3).append("Test methods\n")
271 //      .append(dumpMethods(cls.getTestMethods())).append("</div>\n")
272 //      .append("<div>").append(SP3).append("@BeforeClass\n")
273 //      .append(dumpMethods(cls.getBeforeClassMethods())).append("</div>\n")
274 //      .append("<div>").append(SP3).append("@BeforeMethod\n")
275 //      .append(dumpMethods(cls.getBeforeTestMethods())).append("</div>\n")
276 //      .append("<div>").append(SP3).append("@AfterMethod\n")
277 //      .append(dumpMethods(cls.getAfterTestMethods())).append("</div>\n")
278 //      .append("<div>").append(SP3).append("@AfterClass\n")
279 //      .append(dumpMethods(cls.getAfterClassMethods())).append("</div>\n")
280 //     ;
281 
282     String result = sb.toString();
283     return result;
284   }
285 
dumpMethods(ITestNGMethod[] testMethods)286   private String dumpMethods(ITestNGMethod[] testMethods) {
287     StringBuffer sb = new StringBuffer();
288     if(null == testMethods || testMethods.length == 0) {
289       return "";
290     }
291 
292     for (ITestNGMethod m : testMethods) {
293       sb.append("<tr>\n");
294       sb.append("<td>&nbsp;</td>\n")
295         .append("<td>").append(m.getMethodName()).append("</td>\n")
296         ;
297       String[] groups = m.getGroups();
298       if (groups != null && groups.length > 0) {
299         sb.append("<td>");
300         for (String g : groups) {
301           sb.append(g).append(" ");
302         }
303         sb.append("</td>\n");
304       }
305       else {
306         sb.append("<td>&nbsp;</td>");
307       }
308       sb.append("</tr>\n");
309     }
310 
311 //    StringBuffer sb = new StringBuffer("<br/>");  //"<table bgcolor=\"#c0c0c0\"/>");
312 //    for (ITestNGMethod tm : testMethods) {
313 //      sb
314 //      .append(SP4).append(tm.getMethodName()).append("()\n")
315 //      .append(dumpGroups(tm.getGroups()))
316 //      .append("<br/>");
317 //      ;
318 //    }
319 
320 
321     String result = sb.toString();
322     return result;
323   }
324 
dumpGroups(String[] groups)325   private String dumpGroups(String[] groups) {
326     StringBuffer sb = new StringBuffer();
327 
328     if (null != groups && groups.length > 0) {
329       sb.append(SP4).append("<em>[");
330 
331       for (String g : groups) {
332         sb.append(g).append(" ");
333       }
334 
335       sb.append("]</em><br/>\n");
336     }
337 
338     String result = sb.toString();
339     return result;
340   }
341 
342   /**
343    * Generate information about the methods that were run
344    */
345   public static final String AFTER= "&lt;&lt;";
346   public static final String BEFORE = "&gt;&gt;";
generateMethodsChronologically(XmlSuite xmlSuite, ISuite suite, String outputFileName, boolean alphabetical)347   private void generateMethodsChronologically(XmlSuite xmlSuite, ISuite suite,
348       String outputFileName, boolean alphabetical)
349   {
350     try (BufferedWriter bw = Utils.openWriter(getOutputDirectory(xmlSuite), outputFileName)) {
351       bw.append("<h2>Methods run, sorted chronologically</h2>");
352       bw.append("<h3>" + BEFORE + " means before, " + AFTER + " means after</h3><p/>");
353 
354       long startDate = -1;
355       bw.append("<br/><em>").append(suite.getName()).append("</em><p/>");
356       bw.append("<small><i>(Hover the method name to see the test class name)</i></small><p/>\n");
357 
358       Collection<IInvokedMethod> invokedMethods = suite.getAllInvokedMethods();
359       if (alphabetical) {
360 	@SuppressWarnings({"unchecked"})
361 	Comparator<? super ITestNGMethod>  alphabeticalComparator = new Comparator(){
362 	  @Override
363 	  public int compare(Object o1, Object o2) {
364 	    IInvokedMethod m1 = (IInvokedMethod) o1;
365 	    IInvokedMethod m2 = (IInvokedMethod) o2;
366 	    return m1.getTestMethod().getMethodName().compareTo(m2.getTestMethod().getMethodName());
367 	  }
368 	};
369 	Collections.sort((List) invokedMethods, alphabeticalComparator);
370       }
371 
372       SimpleDateFormat format = new SimpleDateFormat("yy/MM/dd HH:mm:ss");
373       boolean addedHeader = false;
374       for (IInvokedMethod iim : invokedMethods) {
375 	ITestNGMethod tm = iim.getTestMethod();
376 	if (!addedHeader) {
377 	  bw.append("<table border=\"1\">\n")
378 	    .append("<tr>")
379 	    .append("<th>Time</th>")
380 	    .append("<th>Delta (ms)</th>")
381 	    .append("<th>Suite<br>configuration</th>")
382 	    .append("<th>Test<br>configuration</th>")
383 	    .append("<th>Class<br>configuration</th>")
384 	    .append("<th>Groups<br>configuration</th>")
385 	    .append("<th>Method<br>configuration</th>")
386 	    .append("<th>Test<br>method</th>")
387 	    .append("<th>Thread</th>")
388 	    .append("<th>Instances</th>")
389 	    .append("</tr>\n");
390 	  addedHeader = true;
391 	}
392 	String methodName = tm.toString();
393 	boolean bc = tm.isBeforeClassConfiguration();
394 	boolean ac = tm.isAfterClassConfiguration();
395 	boolean bt = tm.isBeforeTestConfiguration();
396 	boolean at = tm.isAfterTestConfiguration();
397 	boolean bs = tm.isBeforeSuiteConfiguration();
398 	boolean as = tm.isAfterSuiteConfiguration();
399 	boolean bg = tm.isBeforeGroupsConfiguration();
400 	boolean ag = tm.isAfterGroupsConfiguration();
401 	boolean setUp = tm.isBeforeMethodConfiguration();
402 	boolean tearDown = tm.isAfterMethodConfiguration();
403 	boolean isClassConfiguration = bc || ac;
404 	boolean isGroupsConfiguration = bg || ag;
405 	boolean isTestConfiguration = bt || at;
406 	boolean isSuiteConfiguration = bs || as;
407 	boolean isSetupOrTearDown = setUp || tearDown;
408 	String configurationClassMethod = isClassConfiguration ? (bc ? BEFORE : AFTER) + methodName : SP;
409 	String configurationTestMethod = isTestConfiguration ? (bt ? BEFORE : AFTER) + methodName : SP;
410 	String configurationGroupsMethod = isGroupsConfiguration ? (bg ? BEFORE : AFTER) + methodName : SP;
411 	String configurationSuiteMethod = isSuiteConfiguration ? (bs ? BEFORE : AFTER) + methodName : SP;
412 	String setUpOrTearDownMethod = isSetupOrTearDown ? (setUp ? BEFORE : AFTER) + methodName : SP;
413 	String testMethod = tm.isTest() ? methodName : SP;
414 
415 	StringBuffer instances = new StringBuffer();
416 	for (long o : tm.getInstanceHashCodes()) {
417 	  instances.append(o).append(" ");
418 	}
419 
420 	if (startDate == -1) {
421 	  startDate = iim.getDate();
422 	}
423 	String date = format.format(iim.getDate());
424 	bw.append("<tr bgcolor=\"" + createColor(tm) + "\">")
425 	  .append("  <td>").append(date).append("</td> ")
426 	  .append("  <td>").append(Long.toString(iim.getDate() - startDate)).append("</td> ")
427 	  .append(td(configurationSuiteMethod))
428 	  .append(td(configurationTestMethod))
429 	  .append(td(configurationClassMethod))
430 	  .append(td(configurationGroupsMethod))
431 	  .append(td(setUpOrTearDownMethod))
432 	  .append(td(testMethod))
433 	  .append("  <td>").append(tm.getId()).append("</td> ")
434 	  .append("  <td>").append(instances).append("</td> ")
435 	  .append("</tr>\n")
436 	  ;
437       }
438       bw.append("</table>\n");
439     } catch (IOException e) {
440       Utils.log("[SuiteHTMLReporter]", 1, "Error writing to " + outputFileName + ": " + e.getMessage());
441     }
442   }
443 
444   /**
445    * Generate a HTML color based on the class of the method
446    */
createColor(ITestNGMethod tm)447   private String createColor(ITestNGMethod tm) {
448     // real class can be null if this client is remote (not serializable)
449     long color = tm.getRealClass() != null ? tm.getRealClass().hashCode() & 0xffffff: 0xffffff;
450     long[] rgb = {
451         ((color & 0xff0000) >> 16) & 0xff,
452         ((color & 0x00ff00) >> 8) & 0xff,
453         color & 0xff
454     };
455     // Not too dark
456     for (int i = 0; i < rgb.length; i++) {
457       if (rgb[i] < 0x60) {
458         rgb[i] += 0x60;
459       }
460     }
461     long adjustedColor = (rgb[0] << 16) | (rgb[1] << 8) | rgb[2];
462     String result = Long.toHexString(adjustedColor);
463 
464     return result;
465   }
466 
td(String s)467   private String td(String s) {
468     StringBuffer result = new StringBuffer();
469     String prefix = "";
470 
471     if (s.startsWith(BEFORE)) {
472       prefix = BEFORE;
473     }
474     else if (s.startsWith(AFTER)) {
475       prefix = AFTER;
476     }
477 
478     if (! s.equals(SP)) {
479       result.append("<td title=\"").append(s).append("\">");
480       int open = s.lastIndexOf("(");
481       int start = s.substring(0, open).lastIndexOf(".");
482 //      int end = s.lastIndexOf(")");
483       if (start >= 0) {
484         result.append(prefix + s.substring(start + 1, open));
485       }
486       else {
487         result.append(prefix + s);
488       }
489       result.append("</td> \n");
490     }
491     else {
492       result.append("<td>").append(SP).append("</td>");
493     }
494 
495     return result.toString();
496   }
497 
ppp(String s)498   private void ppp(String s) {
499     System.out.println("[SuiteHTMLReporter] " + s);
500   }
501 
502   /**
503    * Generate information about methods and groups
504    */
generateMethodsAndGroups(XmlSuite xmlSuite, ISuite suite)505   private void generateMethodsAndGroups(XmlSuite xmlSuite, ISuite suite) {
506     StringBuffer sb = new StringBuffer();
507 
508     Map<String, Collection<ITestNGMethod>> groups = suite.getMethodsByGroups();
509 
510     sb.append("<h2>Groups used for this test run</h2>");
511     if (groups.size() > 0) {
512       sb.append("<table border=\"1\">\n")
513         .append("<tr> <td align=\"center\"><b>Group name</b></td>")
514         .append("<td align=\"center\"><b>Methods</b></td></tr>");
515 
516       String[] groupNames = groups.keySet().toArray(new String[groups.size()]);
517       Arrays.sort(groupNames);
518       for (String group : groupNames) {
519         Collection<ITestNGMethod> methods = groups.get(group);
520         sb.append("<tr><td>").append(group).append("</td>");
521         StringBuffer methodNames = new StringBuffer();
522         Map<ITestNGMethod, ITestNGMethod> uniqueMethods = Maps.newHashMap();
523         for (ITestNGMethod tm : methods) {
524           uniqueMethods.put(tm, tm);
525         }
526         for (ITestNGMethod tm : uniqueMethods.values()) {
527           methodNames.append(tm.toString()).append("<br/>");
528         }
529         sb.append("<td>" + methodNames.toString() + "</td></tr>\n");
530       }
531 
532       sb.append("</table>\n");
533     }
534     Utils.writeFile(getOutputDirectory(xmlSuite), GROUPS, sb.toString());
535   }
536 
generateIndex(XmlSuite xmlSuite, ISuite sr)537   private void generateIndex(XmlSuite xmlSuite, ISuite sr) {
538     StringBuffer index = new StringBuffer()
539     .append("<html><head><title>Results for " + sr.getName() + "</title></head>\n")
540     .append("<frameset cols=\"26%,74%\">\n")
541     .append("<frame src=\"toc.html\" name=\"navFrame\">\n")
542     .append("<frame src=\"main.html\" name=\"mainFrame\">\n")
543     .append("</frameset>\n")
544     .append("</html>\n")
545     ;
546 
547     Utils.writeFile(getOutputDirectory(xmlSuite), "index.html", index.toString());
548   }
549 
makeTitle(ISuite suite)550   private String makeTitle(ISuite suite) {
551     return "Results for<br/><em>" + suite.getName() + "</em>";
552   }
553 
generateMain(XmlSuite xmlSuite, ISuite sr)554   private void generateMain(XmlSuite xmlSuite, ISuite sr) {
555     StringBuffer index = new StringBuffer()
556     .append("<html><head><title>Results for " + sr.getName() + "</title></head>\n")
557     .append("<body>Select a result on the left-hand pane.</body>")
558     .append("</html>\n")
559     ;
560 
561     Utils.writeFile(getOutputDirectory(xmlSuite), "main.html", index.toString());
562   }
563 
564   /**
565    *
566    */
generateTableOfContents(XmlSuite xmlSuite, ISuite suite)567   private void generateTableOfContents(XmlSuite xmlSuite, ISuite suite) {
568     StringBuffer tableOfContents = new StringBuffer();
569 
570     //
571     // Generate methods and groups hyperlinks
572     //
573     Map<String, ISuiteResult> suiteResults = suite.getResults();
574     int groupCount = suite.getMethodsByGroups().size();
575     int methodCount = 0;
576     for (ISuiteResult sr : suiteResults.values()) {
577       ITestNGMethod[] methods = sr.getTestContext().getAllTestMethods();
578       methodCount += Utils.calculateInvokedMethodCount(methods);
579 
580       // Collect testClasses
581       for (ITestNGMethod tm : methods) {
582         ITestClass tc = tm.getTestClass();
583         m_classes.put(tc.getRealClass().getName(), tc);
584       }
585     }
586 
587     String name = "Results for " + suite.getName();
588     tableOfContents
589         .append("<html>\n")
590         .append("<head>\n")
591         .append("<title>" + name + "</title>\n")
592         .append(HtmlHelper.getCssString())
593         .append("</head>\n")
594         ;
595     tableOfContents
596         .append("<body>\n")
597         .append("<h3><p align=\"center\">" + makeTitle(suite) + "</p></h3>\n")
598         .append("<table border='1' width='100%'>\n")
599         .append("<tr valign='top'>\n")
600           .append("<td>")
601             .append(suiteResults.size()).append(" ").append(pluralize(suiteResults.size(), "test"))
602           .append("</td>\n")
603           .append("<td>")
604               .append("<a target='mainFrame' href='").append(CLASSES).append("'>")
605               .append(m_classes.size() + " " + pluralize(m_classes.size(), "class"))
606               .append("</a>")
607           .append("</td>\n")
608           .append("<td>" + methodCount + " " + pluralize(methodCount, "method") + ":<br/>\n")
609             .append("&nbsp;&nbsp;<a target='mainFrame' href='").append(METHODS_CHRONOLOGICAL).append("'>").append("chronological</a><br/>\n")
610             .append("&nbsp;&nbsp;<a target='mainFrame' href='").append(METHODS_ALPHABETICAL).append("\'>").append("alphabetical</a><br/>\n")
611             .append("&nbsp;&nbsp;<a target='mainFrame' href='").append(METHODS_NOT_RUN).append("'>not run (" + suite.getExcludedMethods().size() + ")</a>")
612           .append("</td>\n")
613         .append("</tr>\n")
614 
615         .append("<tr>\n")
616         .append("<td><a target='mainFrame' href='").append(GROUPS).append("'>").append(groupCount + pluralize(groupCount, " group") + "</a></td>\n")
617         .append("<td><a target='mainFrame' href='").append(REPORTER_OUTPUT).append("'>reporter output</a></td>\n")
618         .append("<td><a target='mainFrame' href='").append(TESTNG_XML).append("'>testng.xml</a></td>\n")
619         .append("</tr>")
620         .append("</table>");
621 
622       //
623       // Generate results for individual tests
624       //
625 
626       // Order the results so we can show the failures first, then the skip and
627       // finally the successes
628       Map<String, ISuiteResult> redResults = Maps.newHashMap();
629       Map<String, ISuiteResult> yellowResults = Maps.newHashMap();
630       Map<String, ISuiteResult> greenResults = Maps.newHashMap();
631 
632       for (Map.Entry<String, ISuiteResult> entry : suiteResults.entrySet()) {
633         String suiteName = entry.getKey();
634         ISuiteResult sr = entry.getValue();
635         ITestContext tc = sr.getTestContext();
636         int failed = tc.getFailedTests().size();
637         int skipped = tc.getSkippedTests().size();
638         int passed = tc.getPassedTests().size();
639 
640         if (failed > 0) {
641           redResults.put(suiteName, sr);
642         }
643         else if (skipped > 0) {
644           yellowResults.put(suiteName, sr);
645         }
646         else if (passed > 0) {
647           greenResults.put(suiteName, sr);
648         }
649         else {
650           redResults.put(suiteName, sr);
651         }
652       }
653 
654 
655       ISuiteResult[][] results = new ISuiteResult[][] {
656         sortResults(redResults.values()), sortResults(yellowResults.values()), sortResults(greenResults.values())
657       };
658 
659       String[] colors = {"failed", "skipped", "passed"};
660       for (int i = 0; i < colors.length; i++) {
661         ISuiteResult[] r = results[i];
662         for (ISuiteResult sr: r) {
663           String suiteName = sr.getTestContext().getName();
664           generateSuiteResult(suiteName, sr, colors[i], tableOfContents, m_outputDirectory);
665         }
666       }
667 
668     tableOfContents.append("</body></html>");
669     Utils.writeFile(getOutputDirectory(xmlSuite), "toc.html", tableOfContents.toString());
670   }
671 
pluralize(int count, String singular)672   private String pluralize(int count, String singular) {
673     return count > 1 ? (singular.endsWith("s") ? singular + "es" : singular + "s") : singular;
674   }
675 
getOutputDirectory(XmlSuite xmlSuite)676   private String getOutputDirectory(XmlSuite xmlSuite) {
677     return m_outputDirectory + File.separatorChar + xmlSuite.getName();
678   }
679 
sortResults(Collection<ISuiteResult> r)680   private ISuiteResult[] sortResults(Collection<ISuiteResult> r) {
681     ISuiteResult[] result = r.toArray(new ISuiteResult[r.size()]);
682     Arrays.sort(result);
683     return result;
684   }
685 
generateSuiteResult(String suiteName, ISuiteResult sr, String cssClass, StringBuffer tableOfContents, String outputDirectory)686   private void generateSuiteResult(String suiteName,
687                                    ISuiteResult sr,
688                                    String cssClass,
689                                    StringBuffer tableOfContents,
690                                    String outputDirectory)
691   {
692     ITestContext tc = sr.getTestContext();
693     int passed = tc.getPassedTests().size();
694     int failed = tc.getFailedTests().size();
695     int skipped = tc.getSkippedTests().size();
696     String baseFile = tc.getName();
697     tableOfContents
698       .append("\n<table width='100%' class='test-").append(cssClass).append("'>\n")
699       .append("<tr><td>\n")
700       .append("<table style='width: 100%'><tr>")
701       .append("<td valign='top'>")
702       .append(suiteName).append(" (").append(passed).append("/").append(failed).append("/").append(skipped).append(")")
703       .append("</td>")
704       .append("<td valign='top' align='right'>\n")
705       .append("  <a href='" + baseFile + ".html' target='mainFrame'>Results</a>\n")
706 //      .append("  <a href=\"" + baseFile + ".out\" target=\"mainFrame\"\">Output</a>\n")
707 //      .append("&nbsp;&nbsp;<a href=\"file://" + baseFile + ".properties\" target=\"mainFrame\"\">Property file</a><br>\n")
708       .append("</td>")
709       .append("</tr></table>\n")
710       .append("</td></tr><p/>\n")
711       ;
712 
713     tableOfContents.append("</table>\n");
714   }
715 
716   /**
717    * Writes a property file for each suite result.
718    *
719    * @param xmlSuite
720    * @param suite
721    */
generateSuites(XmlSuite xmlSuite, ISuite suite)722   private void generateSuites(XmlSuite xmlSuite, ISuite suite) {
723     Map<String, ISuiteResult> suiteResults = suite.getResults();
724 
725     for (ISuiteResult sr : suiteResults.values()) {
726       ITestContext testContext = sr.getTestContext();
727       StringBuffer sb = new StringBuffer();
728 
729       for (ISuiteResult suiteResult : suiteResults.values()) {
730         sb.append(suiteResult.toString());
731       }
732       Utils.writeFile(getOutputDirectory(xmlSuite), testContext.getName() + ".properties", sb.toString());
733     }
734   }
735 }
736