001: package org.testng.reporters;
002:
003: import java.io.BufferedWriter;
004: import java.io.File;
005: import java.io.FileWriter;
006: import java.io.IOException;
007: import java.io.PrintWriter;
008: import java.text.DecimalFormat;
009: import java.text.NumberFormat;
010: import java.util.Collection;
011: import java.util.Comparator;
012: import java.util.List;
013: import java.util.Map;
014: import java.util.Set;
015: import java.util.TreeSet;
016:
017: import org.testng.IReporter;
018: import org.testng.IResultMap;
019: import org.testng.ISuite;
020: import org.testng.ISuiteResult;
021: import org.testng.ITestContext;
022: import org.testng.ITestNGMethod;
023: import org.testng.ITestResult;
024: import org.testng.Reporter;
025: import org.testng.log4testng.Logger;
026: import org.testng.reporters.util.StackTraceTools;
027: import org.testng.xml.XmlSuite;
028:
029: /**
030: * Reported designed to render self-contained HTML top down view of a testing
031: * suite.
032: *
033: * @author Paul Mendelson
034: * @since 5.2
035: * @version $Revision: 347 $
036: */
037: public class EmailableReporter implements IReporter {
038: private static final Logger L = Logger
039: .getLogger(EmailableReporter.class);
040:
041: // ~ Instance fields ------------------------------------------------------
042:
043: private PrintWriter m_out;
044:
045: private int m_row;
046:
047: private int m_methodIndex;
048:
049: private int m_rowTotal;
050:
051: // ~ Methods --------------------------------------------------------------
052:
053: /** Creates summary of the run */
054: public void generateReport(List<XmlSuite> xml, List<ISuite> suites,
055: String outdir) {
056: try {
057: m_out = createWriter(outdir);
058: } catch (IOException e) {
059: L.error("output file", e);
060: return;
061: }
062: startHtml(m_out);
063: generateSuiteSummaryReport(suites);
064: generateMethodSummaryReport(suites);
065: generateMethodDetailReport(suites);
066: endHtml(m_out);
067: m_out.flush();
068: m_out.close();
069: }
070:
071: protected PrintWriter createWriter(String outdir)
072: throws IOException {
073: return new PrintWriter(new BufferedWriter(new FileWriter(
074: new File(outdir, "emailable-report.html"))));
075: }
076:
077: /** Creates a table showing the highlights of each test method with links to the method details */
078: protected void generateMethodSummaryReport(List<ISuite> suites) {
079: m_methodIndex = 0;
080: m_out.println("<a id=\"summary\"></a>");
081: startResultSummaryTable("passed");
082: for (ISuite suite : suites) {
083: if (suites.size() > 1) {
084: titleRow(suite.getName(), 4);
085: }
086: Map<String, ISuiteResult> r = suite.getResults();
087: for (ISuiteResult r2 : r.values()) {
088: ITestContext test = r2.getTestContext();
089: resultSummary(test.getFailedConfigurations(), test
090: .getName(), "failed",
091: " (configuration methods)");
092: resultSummary(test.getFailedTests(), test.getName(),
093: "failed", "");
094: resultSummary(test.getSkippedConfigurations(), test
095: .getName(), "skipped",
096: " (configuration methods)");
097: resultSummary(test.getSkippedTests(), test.getName(),
098: "skipped", "");
099: resultSummary(test.getPassedTests(), test.getName(),
100: "passed", "");
101: }
102: }
103: m_out.println("</table>");
104: }
105:
106: /** Creates a section showing known results for each method */
107: protected void generateMethodDetailReport(List<ISuite> suites) {
108: m_methodIndex = 0;
109: for (ISuite suite : suites) {
110: Map<String, ISuiteResult> r = suite.getResults();
111: for (ISuiteResult r2 : r.values()) {
112: if (r.values().size() > 0) {
113: m_out.println("<h1>"
114: + r2.getTestContext().getName() + "</h1>");
115: }
116: resultDetail(r2.getTestContext()
117: .getFailedConfigurations(), "failed");
118: resultDetail(r2.getTestContext().getFailedTests(),
119: "failed");
120: resultDetail(r2.getTestContext()
121: .getSkippedConfigurations(), "skipped");
122: resultDetail(r2.getTestContext().getSkippedTests(),
123: "skipped");
124: resultDetail(r2.getTestContext().getPassedTests(),
125: "passed");
126: }
127: }
128: }
129:
130: /**
131: * @param tests
132: */
133: private void resultSummary(IResultMap tests, String testname,
134: String style, String details) {
135: if (tests.getAllResults().size() > 0) {
136: StringBuffer buff = new StringBuffer();
137: String lastc = "";
138: int mq = 0;
139: int cq = 0;
140: for (ITestNGMethod method : getMethodSet(tests)) {
141: m_row += 1;
142: m_methodIndex += 1;
143: String cname = method.getTestClass().getName();
144: if (mq == 0) {
145: titleRow(testname + " — " + style + details,
146: 4);
147: }
148: if (!cname.equalsIgnoreCase(lastc)) {
149: if (mq > 0) {
150: cq += 1;
151: m_out.println("<tr class=\"" + style
152: + (cq % 2 == 0 ? "even" : "odd")
153: + "\">" + "<td rowspan=\"" + mq + "\">"
154: + lastc + buff);
155: }
156: mq = 0;
157: buff.setLength(0);
158: lastc = cname;
159: }
160: Set<ITestResult> result_set = tests.getResults(method);
161: long end = Long.MIN_VALUE;
162: long start = Long.MAX_VALUE;
163: for (ITestResult ans : tests.getResults(method)) {
164: if (ans.getEndMillis() > end) {
165: end = ans.getEndMillis();
166: }
167: if (ans.getStartMillis() < start) {
168: start = ans.getStartMillis();
169: }
170: }
171: mq += 1;
172: if (mq > 1) {
173: buff.append("<tr class=\"" + style
174: + (cq % 2 == 0 ? "odd" : "even") + "\">");
175: }
176: buff.append("<td><a href=\"#m" + m_methodIndex + "\">"
177: + qualifiedName(method) + "</a></td>"
178: + "<td class=\"numi\">" + result_set.size()
179: + "</td><td class=\"numi\">" + (end - start)
180: + "</td></tr>");
181: }
182: if (mq > 0) {
183: cq += 1;
184: m_out.println("<tr class=\"" + style
185: + (cq % 2 == 0 ? "even" : "odd") + "\">"
186: + "<td rowspan=\"" + mq + "\">" + lastc + buff);
187: }
188: }
189: }
190:
191: /** Starts and defines columns result summary table */
192: private void startResultSummaryTable(String style) {
193: tableStart(style);
194: m_out
195: .println("<tr><th>Class</th>"
196: + "<th>Method</th><th># of<br/>Scenarios</th><th>Time<br/>(Msecs)</th></tr>");
197: m_row = 0;
198: }
199:
200: private String qualifiedName(ITestNGMethod method) {
201: String addon = "";
202: if (method.getGroups().length > 0
203: && !"basic".equalsIgnoreCase(method.getGroups()[0])) {
204: addon = " (" + method.getGroups()[0] + ")";
205: }
206: return method.getMethodName() + addon;
207: }
208:
209: private void resultDetail(IResultMap tests, final String style) {
210: if (tests.getAllResults().size() > 0) {
211: int row = 0;
212: for (ITestNGMethod method : getMethodSet(tests)) {
213: row += 1;
214: m_methodIndex += 1;
215: String cname = method.getTestClass().getName();
216: m_out.println("<a id=\"m" + m_methodIndex
217: + "\"></a><h2>" + cname + ":"
218: + method.getMethodName() + "</h2>");
219: int rq = 0;
220: Set<ITestResult> resultSet = tests.getResults(method);
221: for (ITestResult ans : resultSet) {
222: rq += 1;
223: Object[] parameters = ans.getParameters();
224: boolean hasParameters = parameters != null
225: && parameters.length > 0;
226: if (hasParameters) {
227: if (rq == 1) {
228: tableStart("param");
229: m_out.print("<tr>");
230: for (int x = 1; x <= parameters.length; x++) {
231: m_out
232: .print("<th style=\"padding-left:1em;padding-right:1em\">Parameter #"
233: + x + "</th>");
234: }
235: m_out.println("</tr>");
236: }
237: m_out.print("<tr"
238: + (rq % 2 == 0 ? " class=\"stripe\""
239: : "") + ">");
240: for (Object p : parameters) {
241: m_out
242: .println("<td style=\"padding-left:.5em;padding-right:2em\">"
243: + (p != null ? p.toString()
244: : "null") + "</td>");
245: }
246: m_out.println("</tr>");
247: }
248: List<String> msgs = Reporter.getOutput(ans);
249: boolean hasReporterOutput = msgs.size() > 0;
250: Throwable exception = ans.getThrowable();
251: boolean hasThrowable = exception != null;
252: if (hasReporterOutput || hasThrowable) {
253: String indent = " style=\"padding-left:3em\"";
254: if (hasParameters) {
255: m_out
256: .println("<tr"
257: + (rq % 2 == 0 ? " class=\"stripe\""
258: : "") + "><td"
259: + indent + " colspan=\""
260: + parameters.length + "\">");
261: } else {
262: m_out.println("<div" + indent + ">");
263: }
264: if (hasReporterOutput) {
265: if (hasThrowable)
266: m_out.println("<h3>Test Messages</h3>");
267: for (String line : msgs) {
268: m_out.println(line + "<br/>");
269: }
270: }
271: if (hasThrowable) {
272: boolean wantsMinimalOutput = ans
273: .getStatus() == ITestResult.SUCCESS;
274: if (hasReporterOutput)
275: m_out
276: .println("<h3>"
277: + (wantsMinimalOutput ? "Expected Exception"
278: : "Failure")
279: + "</h3>");
280: generateExceptionReport(exception, method);
281: }
282: if (hasParameters) {
283: m_out.println("</td></tr>");
284: } else {
285: m_out.println("</div>");
286: }
287: }
288: if (hasParameters) {
289: if (rq == resultSet.size()) {
290: m_out.println("</table>");
291: }
292: }
293: }
294: m_out
295: .println("<p class=\"totop\"><a href=\"#summary\">back to summary</a></p>");
296: }
297: }
298: }
299:
300: protected void generateExceptionReport(Throwable exception,
301: ITestNGMethod method) {
302: generateExceptionReport(exception, method, exception
303: .getLocalizedMessage());
304: }
305:
306: private void generateExceptionReport(Throwable exception,
307: ITestNGMethod method, String title) {
308: m_out.println("<p>" + escape(title) + "</p>");
309: StackTraceElement[] s1 = exception.getStackTrace();
310: Throwable t2 = exception.getCause();
311: if (t2 == exception) {
312: t2 = null;
313: }
314: int maxlines = Math.min(100, StackTraceTools.getTestRoot(s1,
315: method));
316: for (int x = 0; x <= maxlines; x++) {
317: m_out.println((x > 0 ? "<br/>at " : "")
318: + escape(s1[x].toString()));
319: }
320: if (maxlines < s1.length) {
321: m_out.println("<br/>" + (s1.length - maxlines)
322: + " lines not shown");
323: }
324: if (t2 != null) {
325: generateExceptionReport(t2, method, "Caused by "
326: + t2.getLocalizedMessage());
327: }
328: }
329:
330: private static String escape(String string) {
331: if (null == string)
332: return string;
333: return string.replaceAll("<", "<").replaceAll(">", ">");
334: }
335:
336: /**
337: * @param tests
338: * @return
339: */
340: private Collection<ITestNGMethod> getMethodSet(IResultMap tests) {
341: Set r = new TreeSet<ITestNGMethod>(
342: new TestSorter<ITestNGMethod>());
343: r.addAll(tests.getAllMethods());
344: return r;
345: }
346:
347: public void generateSuiteSummaryReport(List<ISuite> suites) {
348: tableStart("param");
349: m_out.print("<tr><th>Test</th>");
350: tableColumnStart("Methods<br/>Passed");
351: tableColumnStart("Scenarios<br/>Passed");
352: tableColumnStart("# skipped");
353: tableColumnStart("# failed");
354: tableColumnStart("Total<br/>Time");
355: tableColumnStart("Included<br/>Groups");
356: tableColumnStart("Excluded<br/>Groups");
357: m_out.println("</tr>");
358: NumberFormat formatter = new DecimalFormat("#,##0.0");
359: int qty_tests = 0;
360: int qty_pass_m = 0;
361: int qty_pass_s = 0;
362: int qty_skip = 0;
363: int qty_fail = 0;
364: long time_start = Long.MAX_VALUE;
365: long time_end = Long.MIN_VALUE;
366: for (ISuite suite : suites) {
367: if (suites.size() > 1) {
368: titleRow(suite.getName(), 7);
369: }
370: Map<String, ISuiteResult> tests = suite.getResults();
371: for (ISuiteResult r : tests.values()) {
372: qty_tests += 1;
373: ITestContext overview = r.getTestContext();
374: startSummaryRow(overview.getName());
375: int q = getMethodSet(overview.getPassedTests()).size();
376: qty_pass_m += q;
377: summaryCell(q, Integer.MAX_VALUE);
378: q = overview.getPassedTests().size();
379: qty_pass_s += q;
380: summaryCell(q, Integer.MAX_VALUE);
381: q = getMethodSet(overview.getSkippedTests()).size();
382: qty_skip += q;
383: summaryCell(q, 0);
384: q = getMethodSet(overview.getFailedTests()).size();
385: qty_fail += q;
386: summaryCell(q, 0);
387: time_start = Math.min(
388: overview.getStartDate().getTime(), time_start);
389: time_end = Math.max(overview.getEndDate().getTime(),
390: time_end);
391: summaryCell(
392: formatter.format((overview.getEndDate()
393: .getTime() - overview.getStartDate()
394: .getTime()) / 1000.)
395: + " seconds", true);
396: summaryCell(overview.getIncludedGroups());
397: summaryCell(overview.getExcludedGroups());
398: m_out.println("</tr>");
399: }
400: }
401: if (qty_tests > 1) {
402: m_out.println("<tr class=\"total\"><td>Total</td>");
403: summaryCell(qty_pass_m, Integer.MAX_VALUE);
404: summaryCell(qty_pass_s, Integer.MAX_VALUE);
405: summaryCell(qty_skip, 0);
406: summaryCell(qty_fail, 0);
407: summaryCell(formatter
408: .format((time_end - time_start) / 1000.)
409: + " seconds", true);
410: m_out.println("<td colspan=\"2\"> </td></tr>");
411: }
412: m_out.println("</table>");
413: }
414:
415: private void summaryCell(String[] val) {
416: StringBuffer b = new StringBuffer();
417: for (String v : val) {
418: b.append(v + " ");
419: }
420: summaryCell(b.toString(), true);
421: }
422:
423: private void summaryCell(String v, boolean isgood) {
424: m_out.print("<td class=\"numi" + (isgood ? "" : "_attn")
425: + "\">" + v + "</td>");
426: }
427:
428: private void startSummaryRow(String label) {
429: m_row += 1;
430: m_out.print("<tr" + (m_row % 2 == 0 ? " class=\"stripe\"" : "")
431: + "><td style=\"text-align:left;padding-right:2em\">"
432: + label + "</td>");
433: }
434:
435: private void summaryCell(int v, int maxexpected) {
436: summaryCell(String.valueOf(v), v <= maxexpected);
437: m_rowTotal += v;
438: }
439:
440: /**
441: *
442: */
443: private void tableStart(String cssclass) {
444: m_out.println("<table cellspacing=0 cellpadding=0"
445: + (cssclass != null ? " class=\"" + cssclass + "\""
446: : " style=\"padding-bottom:2em\"") + ">");
447: m_row = 0;
448: }
449:
450: private void tableColumnStart(String label) {
451: m_out.print("<th class=\"numi\">" + label + "</th>");
452: }
453:
454: private void titleRow(String label, int cq) {
455: m_out.println("<tr><th colspan=\"" + cq + "\">" + label
456: + "</th></tr>");
457: m_row = 0;
458: }
459:
460: protected void writeStyle(String[] formats, String[] targets) {
461:
462: }
463:
464: /** Starts HTML stream */
465: protected void startHtml(PrintWriter out) {
466: out
467: .println("<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.1//EN http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd\">");
468: out.println("<html xmlns=\"http://www.w3.org/1999/xhtml\">");
469: out.println("<head>");
470: out.println("<title>TestNG: Unit Test</title>");
471: out.println("<style type=\"text/css\">");
472: out
473: .println("table caption,table.info_table,table.param,table.passed,table.failed {margin-bottom:10px;border:1px solid #000099;border-collapse:collapse;empty-cells:show;}");
474: out
475: .println("table.info_table td,table.info_table th,table.param td,table.param th,table.passed td,table.passed th,table.failed td,table.failed th {");
476: out
477: .println("border:1px solid #000099;padding:.25em .5em .25em .5em");
478: out.println("}");
479: out.println("table.param th {vertical-align:bottom}");
480: out.println("td.numi,th.numi,td.numi_attn {");
481: out.println("text-align:right");
482: out.println("}");
483: out.println("tr.total td {font-weight:bold}");
484: out.println("table caption {");
485: out.println("text-align:center;font-weight:bold;");
486: out.println("}");
487: out
488: .println("table.passed tr.stripe td,table tr.passedodd td {background-color: #00AA00;}");
489: out
490: .println("table.passed td,table tr.passedeven td {background-color: #33FF33;}");
491: out
492: .println("table.passed tr.stripe td,table tr.skippedodd td {background-color: #cccccc;}");
493: out
494: .println("table.passed td,table tr.skippedodd td {background-color: #dddddd;}");
495: out
496: .println("table.failed tr.stripe td,table tr.failedodd td,table.param td.numi_attn {background-color: #FF3333;}");
497: out
498: .println("table.failed td,table tr.failedeven td,table.param tr.stripe td.numi_attn {background-color: #DD0000;}");
499: out
500: .println("tr.stripe td,tr.stripe th {background-color: #E6EBF9;}");
501: out
502: .println("p.totop {font-size:85%;text-align:center;border-bottom:2px black solid}");
503: out
504: .println("div.shootout {padding:2em;border:3px #4854A8 solid}");
505: out.println("</style>");
506: out.println("</head>");
507: out.println("<body>");
508: }
509:
510: /** Finishes HTML stream */
511: protected void endHtml(PrintWriter out) {
512: out.println("</body></html>");
513: }
514:
515: // ~ Inner Classes --------------------------------------------------------
516: /** Arranges methods by classname and method name */
517: private class TestSorter<T extends ITestNGMethod> implements
518: Comparator {
519: // ~ Methods -------------------------------------------------------------
520:
521: /** Arranges methods by classname and method name */
522: public int compare(Object o1, Object o2) {
523: int r = ((T) o1).getTestClass().getName().compareTo(
524: ((T) o2).getTestClass().getName());
525: if (r == 0) {
526: r = ((T) o1).getMethodName().compareTo(
527: ((T) o2).getMethodName());
528: }
529: return r;
530: }
531: }
532: }
|