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 + " — " + 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\"> </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