1 package org.testng.reporters;
2 
3 import org.testng.IInvokedMethod;
4 import org.testng.IReporter;
5 import org.testng.IResultMap;
6 import org.testng.ISuite;
7 import org.testng.ISuiteResult;
8 import org.testng.ITestClass;
9 import org.testng.ITestContext;
10 import org.testng.ITestNGMethod;
11 import org.testng.ITestResult;
12 import org.testng.Reporter;
13 import org.testng.collections.Lists;
14 import org.testng.internal.Utils;
15 import org.testng.log4testng.Logger;
16 import org.testng.xml.XmlSuite;
17 
18 import java.io.BufferedWriter;
19 import java.io.File;
20 import java.io.FileWriter;
21 import java.io.IOException;
22 import java.io.PrintWriter;
23 import java.text.DecimalFormat;
24 import java.text.NumberFormat;
25 import java.util.Arrays;
26 import java.util.Collection;
27 import java.util.Comparator;
28 import java.util.List;
29 import java.util.Map;
30 import java.util.Set;
31 
32 /**
33  * Reported designed to render self-contained HTML top down view of a testing
34  * suite.
35  *
36  * @author Paul Mendelson
37  * @since 5.2
38  * @version $Revision: 719 $
39  */
40 public class EmailableReporter implements IReporter {
41   private static final Logger L = Logger.getLogger(EmailableReporter.class);
42 
43   // ~ Instance fields ------------------------------------------------------
44 
45   private PrintWriter m_out;
46 
47   private int m_row;
48 
49   private Integer m_testIndex;
50 
51   private int m_methodIndex;
52 
53   // ~ Methods --------------------------------------------------------------
54 
55   /** Creates summary of the run */
56   @Override
generateReport(List<XmlSuite> xml, List<ISuite> suites, String outdir)57   public void generateReport(List<XmlSuite> xml, List<ISuite> suites, String outdir) {
58     try {
59       m_out = createWriter(outdir);
60     }
61     catch (IOException e) {
62       L.error("output file", e);
63       return;
64     }
65     startHtml(m_out);
66     generateSuiteSummaryReport(suites);
67     generateMethodSummaryReport(suites);
68     generateMethodDetailReport(suites);
69     endHtml(m_out);
70     m_out.flush();
71     m_out.close();
72   }
73 
createWriter(String outdir)74   protected PrintWriter createWriter(String outdir) throws IOException {
75     new File(outdir).mkdirs();
76     return new PrintWriter(new BufferedWriter(new FileWriter(new File(outdir,
77         "emailable-report.html"))));
78   }
79 
80   /** Creates a table showing the highlights of each test method with links to the method details */
generateMethodSummaryReport(List<ISuite> suites)81   protected void generateMethodSummaryReport(List<ISuite> suites) {
82     m_methodIndex = 0;
83     startResultSummaryTable("methodOverview");
84     int testIndex = 1;
85     for (ISuite suite : suites) {
86       if(suites.size()>1) {
87         titleRow(suite.getName(), 5);
88       }
89       Map<String, ISuiteResult> r = suite.getResults();
90       for (ISuiteResult r2 : r.values()) {
91         ITestContext testContext = r2.getTestContext();
92         String testName = testContext.getName();
93         m_testIndex = testIndex;
94         resultSummary(suite, testContext.getFailedConfigurations(), testName,
95             "failed", " (configuration methods)");
96         resultSummary(suite, testContext.getFailedTests(), testName, "failed",
97             "");
98         resultSummary(suite, testContext.getSkippedConfigurations(), testName,
99             "skipped", " (configuration methods)");
100         resultSummary(suite, testContext.getSkippedTests(), testName,
101             "skipped", "");
102         resultSummary(suite, testContext.getPassedTests(), testName, "passed",
103             "");
104         testIndex++;
105       }
106     }
107     m_out.println("</table>");
108   }
109 
110   /** Creates a section showing known results for each method */
generateMethodDetailReport(List<ISuite> suites)111   protected void generateMethodDetailReport(List<ISuite> suites) {
112     m_methodIndex = 0;
113     for (ISuite suite : suites) {
114       Map<String, ISuiteResult> r = suite.getResults();
115       for (ISuiteResult r2 : r.values()) {
116         ITestContext testContext = r2.getTestContext();
117         if (r.values().size() > 0) {
118           m_out.println("<h1>" + testContext.getName() + "</h1>");
119         }
120         resultDetail(testContext.getFailedConfigurations());
121         resultDetail(testContext.getFailedTests());
122         resultDetail(testContext.getSkippedConfigurations());
123         resultDetail(testContext.getSkippedTests());
124         resultDetail(testContext.getPassedTests());
125       }
126     }
127   }
128 
129   /**
130    * @param tests
131    */
resultSummary(ISuite suite, IResultMap tests, String testname, String style, String details)132   private void resultSummary(ISuite suite, IResultMap tests, String testname, String style,
133       String details) {
134     if (tests.getAllResults().size() > 0) {
135       StringBuffer buff = new StringBuffer();
136       String lastClassName = "";
137       int mq = 0;
138       int cq = 0;
139       for (ITestNGMethod method : getMethodSet(tests, suite)) {
140         m_row += 1;
141         m_methodIndex += 1;
142         ITestClass testClass = method.getTestClass();
143         String className = testClass.getName();
144         if (mq == 0) {
145           String id = (m_testIndex == null ? null : "t" + Integer.toString(m_testIndex));
146           titleRow(testname + " &#8212; " + style + details, 5, id);
147           m_testIndex = null;
148         }
149         if (!className.equalsIgnoreCase(lastClassName)) {
150           if (mq > 0) {
151             cq += 1;
152             m_out.print("<tr class=\"" + style
153                 + (cq % 2 == 0 ? "even" : "odd") + "\">" + "<td");
154             if (mq > 1) {
155               m_out.print(" rowspan=\"" + mq + "\"");
156             }
157             m_out.println(">" + lastClassName + "</td>" + buff);
158           }
159           mq = 0;
160           buff.setLength(0);
161           lastClassName = className;
162         }
163         Set<ITestResult> resultSet = tests.getResults(method);
164         long end = Long.MIN_VALUE;
165         long start = Long.MAX_VALUE;
166         for (ITestResult testResult : tests.getResults(method)) {
167           if (testResult.getEndMillis() > end) {
168             end = testResult.getEndMillis();
169           }
170           if (testResult.getStartMillis() < start) {
171             start = testResult.getStartMillis();
172           }
173         }
174         mq += 1;
175         if (mq > 1) {
176           buff.append("<tr class=\"" + style + (cq % 2 == 0 ? "odd" : "even")
177               + "\">");
178         }
179         String description = method.getDescription();
180         String testInstanceName = resultSet.toArray(new ITestResult[]{})[0].getTestName();
181         buff.append("<td><a href=\"#m" + m_methodIndex + "\">"
182             + qualifiedName(method)
183             + " " + (description != null && description.length() > 0
184                 ? "(\"" + description + "\")"
185                 : "")
186             + "</a>" + (null == testInstanceName ? "" : "<br>(" + testInstanceName + ")")
187             + "</td>"
188             + "<td class=\"numi\">" + resultSet.size() + "</td>"
189             + "<td>" + start + "</td>"
190             + "<td class=\"numi\">" + (end - start) + "</td>"
191             + "</tr>");
192       }
193       if (mq > 0) {
194         cq += 1;
195         m_out.print("<tr class=\"" + style + (cq % 2 == 0 ? "even" : "odd")
196             + "\">" + "<td");
197         if (mq > 1) {
198           m_out.print(" rowspan=\"" + mq + "\"");
199         }
200         m_out.println(">" + lastClassName + "</td>" + buff);
201       }
202     }
203   }
204 
205   /** Starts and defines columns result summary table */
startResultSummaryTable(String style)206   private void startResultSummaryTable(String style) {
207     tableStart(style, "summary");
208     m_out.println("<tr><th>Class</th>"
209             + "<th>Method</th><th># of<br/>Scenarios</th><th>Start</th><th>Time<br/>(ms)</th></tr>");
210     m_row = 0;
211   }
212 
qualifiedName(ITestNGMethod method)213   private String qualifiedName(ITestNGMethod method) {
214     StringBuilder addon = new StringBuilder();
215     String[] groups = method.getGroups();
216     int length = groups.length;
217     if (length > 0 && !"basic".equalsIgnoreCase(groups[0])) {
218       addon.append("(");
219       for (int i = 0; i < length; i++) {
220         if (i > 0) {
221           addon.append(", ");
222         }
223           addon.append(groups[i]);
224         }
225       addon.append(")");
226     }
227 
228     return "<b>" + method.getMethodName() + "</b> " + addon;
229   }
230 
resultDetail(IResultMap tests)231   private void resultDetail(IResultMap tests) {
232     for (ITestResult result : tests.getAllResults()) {
233       ITestNGMethod method = result.getMethod();
234         m_methodIndex++;
235         String cname = method.getTestClass().getName();
236         m_out.println("<h2 id=\"m" + m_methodIndex + "\">" + cname + ":"
237             + method.getMethodName() + "</h2>");
238         Set<ITestResult> resultSet = tests.getResults(method);
239         generateForResult(result, method, resultSet.size());
240         m_out.println("<p class=\"totop\"><a href=\"#summary\">back to summary</a></p>");
241 
242     }
243   }
244 
generateForResult(ITestResult ans, ITestNGMethod method, int resultSetSize)245   private void generateForResult(ITestResult ans, ITestNGMethod method, int resultSetSize) {
246     Object[] parameters = ans.getParameters();
247     boolean hasParameters = parameters != null && parameters.length > 0;
248     if (hasParameters) {
249       tableStart("result", null);
250       m_out.print("<tr class=\"param\">");
251       for (int x = 1; x <= parameters.length; x++) {
252         m_out.print("<th>Parameter #" + x + "</th>");
253       }
254       m_out.println("</tr>");
255       m_out.print("<tr class=\"param stripe\">");
256       for (Object p : parameters) {
257         m_out.println("<td>" + Utils.escapeHtml(Utils.toString(p)) + "</td>");
258       }
259       m_out.println("</tr>");
260     }
261     List<String> msgs = Reporter.getOutput(ans);
262     boolean hasReporterOutput = msgs.size() > 0;
263     Throwable exception=ans.getThrowable();
264     boolean hasThrowable = exception!=null;
265     if (hasReporterOutput||hasThrowable) {
266       if (hasParameters) {
267         m_out.print("<tr><td");
268         if (parameters.length > 1) {
269           m_out.print(" colspan=\"" + parameters.length + "\"");
270         }
271         m_out.println(">");
272       }
273       else {
274         m_out.println("<div>");
275       }
276       if (hasReporterOutput) {
277         if(hasThrowable) {
278           m_out.println("<h3>Test Messages</h3>");
279         }
280         for (String line : msgs) {
281           m_out.println(line + "<br/>");
282         }
283       }
284       if(hasThrowable) {
285         boolean wantsMinimalOutput = ans.getStatus()==ITestResult.SUCCESS;
286         if(hasReporterOutput) {
287           m_out.println("<h3>"
288               +(wantsMinimalOutput?"Expected Exception":"Failure")
289               +"</h3>");
290         }
291         generateExceptionReport(exception,method);
292       }
293       if (hasParameters) {
294         m_out.println("</td></tr>");
295       }
296       else {
297         m_out.println("</div>");
298       }
299     }
300     if (hasParameters) {
301       m_out.println("</table>");
302     }
303   }
304 
generateExceptionReport(Throwable exception,ITestNGMethod method)305   protected void generateExceptionReport(Throwable exception,ITestNGMethod method) {
306     m_out.print("<div class=\"stacktrace\">");
307     m_out.print(Utils.stackTrace(exception, true)[0]);
308     m_out.println("</div>");
309   }
310 
311   /**
312    * Since the methods will be sorted chronologically, we want to return
313    * the ITestNGMethod from the invoked methods.
314    */
getMethodSet(IResultMap tests, ISuite suite)315   private Collection<ITestNGMethod> getMethodSet(IResultMap tests, ISuite suite) {
316     List<IInvokedMethod> r = Lists.newArrayList();
317     List<IInvokedMethod> invokedMethods = suite.getAllInvokedMethods();
318     for (IInvokedMethod im : invokedMethods) {
319       if (tests.getAllMethods().contains(im.getTestMethod())) {
320         r.add(im);
321       }
322     }
323     Arrays.sort(r.toArray(new IInvokedMethod[r.size()]), new TestSorter());
324     List<ITestNGMethod> result = Lists.newArrayList();
325 
326     // Add all the invoked methods
327     for (IInvokedMethod m : r) {
328       result.add(m.getTestMethod());
329     }
330 
331     // Add all the methods that weren't invoked (e.g. skipped) that we
332     // haven't added yet
333     for (ITestNGMethod m : tests.getAllMethods()) {
334       if (!result.contains(m)) {
335         result.add(m);
336       }
337     }
338     return result;
339   }
340 
generateSuiteSummaryReport(List<ISuite> suites)341   public void generateSuiteSummaryReport(List<ISuite> suites) {
342     tableStart("testOverview", null);
343     m_out.print("<tr>");
344     tableColumnStart("Test");
345     tableColumnStart("Methods<br/>Passed");
346     tableColumnStart("Scenarios<br/>Passed");
347     tableColumnStart("# skipped");
348     tableColumnStart("# failed");
349     tableColumnStart("Total<br/>Time");
350     tableColumnStart("Included<br/>Groups");
351     tableColumnStart("Excluded<br/>Groups");
352     m_out.println("</tr>");
353     NumberFormat formatter = new DecimalFormat("#,##0.0");
354     int qty_tests = 0;
355     int qty_pass_m = 0;
356     int qty_pass_s = 0;
357     int qty_skip = 0;
358     int qty_fail = 0;
359     long time_start = Long.MAX_VALUE;
360     long time_end = Long.MIN_VALUE;
361     m_testIndex = 1;
362     for (ISuite suite : suites) {
363       if (suites.size() > 1) {
364         titleRow(suite.getName(), 8);
365       }
366       Map<String, ISuiteResult> tests = suite.getResults();
367       for (ISuiteResult r : tests.values()) {
368         qty_tests += 1;
369         ITestContext overview = r.getTestContext();
370         startSummaryRow(overview.getName());
371         int q = getMethodSet(overview.getPassedTests(), suite).size();
372         qty_pass_m += q;
373         summaryCell(q,Integer.MAX_VALUE);
374         q = overview.getPassedTests().size();
375         qty_pass_s += q;
376         summaryCell(q,Integer.MAX_VALUE);
377         q = getMethodSet(overview.getSkippedTests(), suite).size();
378         qty_skip += q;
379         summaryCell(q,0);
380         q = getMethodSet(overview.getFailedTests(), suite).size();
381         qty_fail += q;
382         summaryCell(q,0);
383         time_start = Math.min(overview.getStartDate().getTime(), time_start);
384         time_end = Math.max(overview.getEndDate().getTime(), time_end);
385         summaryCell(formatter.format(
386             (overview.getEndDate().getTime() - overview.getStartDate().getTime()) / 1000.)
387             + " seconds", true);
388         summaryCell(overview.getIncludedGroups());
389         summaryCell(overview.getExcludedGroups());
390         m_out.println("</tr>");
391         m_testIndex++;
392       }
393     }
394     if (qty_tests > 1) {
395       m_out.println("<tr class=\"total\"><td>Total</td>");
396       summaryCell(qty_pass_m,Integer.MAX_VALUE);
397       summaryCell(qty_pass_s,Integer.MAX_VALUE);
398       summaryCell(qty_skip,0);
399       summaryCell(qty_fail,0);
400       summaryCell(formatter.format((time_end - time_start) / 1000.) + " seconds", true);
401       m_out.println("<td colspan=\"2\">&nbsp;</td></tr>");
402     }
403     m_out.println("</table>");
404   }
405 
summaryCell(String[] val)406   private void summaryCell(String[] val) {
407     StringBuffer b = new StringBuffer();
408     for (String v : val) {
409       b.append(v + " ");
410     }
411     summaryCell(b.toString(),true);
412   }
413 
summaryCell(String v,boolean isgood)414   private void summaryCell(String v,boolean isgood) {
415     m_out.print("<td class=\"numi"+(isgood?"":"_attn")+"\">" + v + "</td>");
416   }
417 
startSummaryRow(String label)418   private void startSummaryRow(String label) {
419     m_row += 1;
420     m_out.print("<tr" + (m_row % 2 == 0 ? " class=\"stripe\"" : "")
421             + "><td style=\"text-align:left;padding-right:2em\"><a href=\"#t"
422             + m_testIndex + "\">" + label + "</a>"
423             + "</td>");
424   }
425 
summaryCell(int v,int maxexpected)426   private void summaryCell(int v,int maxexpected) {
427     summaryCell(String.valueOf(v),v<=maxexpected);
428   }
429 
tableStart(String cssclass, String id)430   private void tableStart(String cssclass, String id) {
431     m_out.println("<table cellspacing=\"0\" cellpadding=\"0\""
432         + (cssclass != null ? " class=\"" + cssclass + "\""
433             : " style=\"padding-bottom:2em\"")
434         + (id != null ? " id=\"" + id + "\"" : "")
435         + ">");
436     m_row = 0;
437   }
438 
tableColumnStart(String label)439   private void tableColumnStart(String label) {
440     m_out.print("<th>" + label + "</th>");
441   }
442 
titleRow(String label, int cq)443   private void titleRow(String label, int cq) {
444     titleRow(label, cq, null);
445   }
446 
titleRow(String label, int cq, String id)447   private void titleRow(String label, int cq, String id) {
448     m_out.print("<tr");
449     if (id != null) {
450       m_out.print(" id=\"" + id + "\"");
451     }
452     m_out.println( "><th colspan=\"" + cq + "\">" + label + "</th></tr>");
453     m_row = 0;
454   }
455 
456   /** Starts HTML stream */
startHtml(PrintWriter out)457   protected void startHtml(PrintWriter out) {
458     out.println("<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.1//EN\" \"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd\">");
459     out.println("<html xmlns=\"http://www.w3.org/1999/xhtml\">");
460     out.println("<head>");
461     out.println("<title>TestNG Report</title>");
462     out.println("<style type=\"text/css\">");
463     out.println("table {margin-bottom:10px;border-collapse:collapse;empty-cells:show}");
464     out.println("td,th {border:1px solid #009;padding:.25em .5em}");
465     out.println(".result th {vertical-align:bottom}");
466     out.println(".param th {padding-left:1em;padding-right:1em}");
467     out.println(".param td {padding-left:.5em;padding-right:2em}");
468     out.println(".stripe td,.stripe th {background-color: #E6EBF9}");
469     out.println(".numi,.numi_attn {text-align:right}");
470     out.println(".total td {font-weight:bold}");
471     out.println(".passedodd td {background-color: #0A0}");
472     out.println(".passedeven td {background-color: #3F3}");
473     out.println(".skippedodd td {background-color: #CCC}");
474     out.println(".skippedodd td {background-color: #DDD}");
475     out.println(".failedodd td,.numi_attn {background-color: #F33}");
476     out.println(".failedeven td,.stripe .numi_attn {background-color: #D00}");
477     out.println(".stacktrace {white-space:pre;font-family:monospace}");
478     out.println(".totop {font-size:85%;text-align:center;border-bottom:2px solid #000}");
479     out.println("</style>");
480     out.println("</head>");
481     out.println("<body>");
482   }
483 
484   /** Finishes HTML stream */
endHtml(PrintWriter out)485   protected void endHtml(PrintWriter out) {
486     out.println("</body></html>");
487   }
488 
489   // ~ Inner Classes --------------------------------------------------------
490   /** Arranges methods by classname and method name */
491   private static final class TestSorter implements Comparator<IInvokedMethod> {
492     // ~ Methods -------------------------------------------------------------
493 
494     /** Arranges methods by classname and method name */
495     @Override
compare(IInvokedMethod o1, IInvokedMethod o2)496     public int compare(IInvokedMethod o1, IInvokedMethod o2) {
497 //      System.out.println("Comparing " + o1.getMethodName() + " " + o1.getDate()
498 //          + " and " + o2.getMethodName() + " " + o2.getDate());
499       return (int) (o1.getDate() - o2.getDate());
500 //      int r = ((T) o1).getTestClass().getName().compareTo(((T) o2).getTestClass().getName());
501 //      if (r == 0) {
502 //        r = ((T) o1).getMethodName().compareTo(((T) o2).getMethodName());
503 //      }
504 //      return r;
505     }
506   }
507 }
508