001 /* 002 * Cobertura - http://cobertura.sourceforge.net/ 003 * 004 * Copyright (C) 2005 Mark Doliner <thekingant@users.sourceforge.net> 005 * 006 * Cobertura is free software; you can redistribute it and/or modify 007 * it under the terms of the GNU General Public License as published 008 * by the Free Software Foundation; either version 2 of the License, 009 * or (at your option) any later version. 010 * 011 * Cobertura is distributed in the hope that it will be useful, but 012 * WITHOUT ANY WARRANTY; without even the implied warranty of 013 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 014 * General Public License for more details. 015 * 016 * You should have received a copy of the GNU General Public License 017 * along with Cobertura; if not, write to the Free Software 018 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 019 * USA 020 */ 021 022 package net.sourceforge.cobertura.reporting.html; 023 024 import java.io.BufferedReader; 025 import java.io.File; 026 import java.io.FileOutputStream; 027 import java.io.FileReader; 028 import java.io.IOException; 029 import java.io.PrintStream; 030 import java.text.DecimalFormat; 031 import java.text.NumberFormat; 032 import java.util.Iterator; 033 import java.util.Set; 034 import java.util.TreeSet; 035 036 import net.sourceforge.cobertura.reporting.Clazz; 037 import net.sourceforge.cobertura.reporting.CoverageReport; 038 import net.sourceforge.cobertura.reporting.Package; 039 import net.sourceforge.cobertura.reporting.Util; 040 import net.sourceforge.cobertura.reporting.html.files.CopyFiles; 041 042 public class HTMLReport 043 { 044 045 private File outputDir; 046 047 private File sourceDir; 048 049 private CoverageReport coverage; 050 051 /** 052 * Create a coverage report 053 */ 054 public HTMLReport(CoverageReport coverage, File outputDir, File sourceDir) 055 throws Exception 056 { 057 this.outputDir = outputDir; 058 this.sourceDir = sourceDir; 059 this.coverage = coverage; 060 061 removeNonexistantClasses(); 062 CopyFiles.copy(outputDir); 063 generatePackageList(); 064 generateClassLists(); 065 generateOverviews(); 066 generateSourceFiles(); 067 } 068 069 /** 070 * Go through the classes in our coverage data and remove any class 071 * for which we don't have source code. These classes are usually 072 * RMI stubs or inner classes. 073 */ 074 private void removeNonexistantClasses() 075 { 076 Iterator iter = coverage.getClasses().iterator(); 077 while (iter.hasNext()) 078 { 079 Clazz clazz = (Clazz)iter.next(); 080 File file = new File(sourceDir, clazz.getLongFileName()); 081 if (!file.isFile()) 082 { 083 coverage.removeClass(clazz); 084 } 085 } 086 } 087 088 private void generatePackageList() throws IOException 089 { 090 File file = new File(outputDir, "frame-packages.html"); 091 PrintStream out = null; 092 093 try 094 { 095 out = new PrintStream(new FileOutputStream(file)); 096 097 out.println("<html>"); 098 out.println("<head>"); 099 out.println("<title>Coverage Report</title>"); 100 out 101 .println("<link title=\"Style\" type=\"text/css\" rel=\"stylesheet\" href=\"css/main.css\" />"); 102 out.println("</head>"); 103 out.println("<body>"); 104 out.println("<h5>Packages</h5>"); 105 out.println("<table width=\"100%\">"); 106 out.println("<tr>"); 107 out 108 .println("<td nowrap=\"nowrap\"><a href=\"frame-summary.html\" onClick='parent.classList.location.href=\"frame-classes.html\"' target=\"summary\">All</a></td>"); 109 out.println("</tr>"); 110 Iterator iter = coverage.getPackages().iterator(); 111 while (iter.hasNext()) 112 { 113 Package pkg = (Package)iter.next(); 114 String url1 = "frame-summary-" + pkg.getName() + ".html"; 115 String url2 = "frame-classes-" + pkg.getName() + ".html"; 116 out.println("<tr>"); 117 out.println("<td nowrap=\"nowrap\"><a href=\"" + url1 118 + "\" onClick='parent.classList.location.href=\"" 119 + url2 + "\"' target=\"summary\">" + pkg.getName() 120 + "</a></td>"); 121 out.println("</tr>"); 122 } 123 out.println("</table>"); 124 out.println("</body>"); 125 out.println("</html>"); 126 } 127 finally 128 { 129 if (out != null) 130 { 131 out.close(); 132 } 133 } 134 } 135 136 private void generateClassLists() throws IOException 137 { 138 generateClassList(null); 139 Iterator iter = coverage.getPackages().iterator(); 140 while (iter.hasNext()) 141 { 142 Package pkg = (Package)iter.next(); 143 generateClassList(pkg); 144 } 145 } 146 147 private void generateClassList(Package pkg) throws IOException 148 { 149 String filename; 150 Set classes; 151 if (pkg == null) 152 { 153 filename = "frame-classes.html"; 154 classes = coverage.getClasses(); 155 } 156 else 157 { 158 filename = "frame-classes-" + pkg.getName() + ".html"; 159 classes = pkg.getClasses(); 160 } 161 File file = new File(outputDir, filename); 162 PrintStream out = null; 163 164 try 165 { 166 out = new PrintStream(new FileOutputStream(file)); 167 168 out.println("<html>"); 169 out.println("<head>"); 170 out.println("<title>Coverage Report Classes</title>"); 171 out 172 .println("<link title=\"Style\" type=\"text/css\" rel=\"stylesheet\" href=\"css/main.css\" />"); 173 out.println("</head>"); 174 out.println("<body>"); 175 out.println("<h5>"); 176 out.println(pkg == null ? "All Packages" : pkg.getName()); 177 out.println("</h5>"); 178 out.println("<h5>Classes</h5>"); 179 out.println("<table width=\"100%\">"); 180 181 for (Iterator iter = classes.iterator(); iter.hasNext();) 182 { 183 Clazz clazz = (Clazz)iter.next(); 184 out.println("<tr>"); 185 String percentCovered; 186 if (clazz.getNumberOfLines() > 0) 187 percentCovered = getPercentValue(clazz 188 .getLineCoverageRate()); 189 else 190 percentCovered = "N/A"; 191 out 192 .println("<td nowrap=\"nowrap\"><a target=\"summary\" href=\"" 193 + clazz.getLongName() 194 + ".html\">" 195 + clazz.getName() 196 + "</a> <i>(" 197 + percentCovered + ")</i></td>"); 198 out.println("</tr>"); 199 } 200 201 out.println("</td>"); 202 out.println("</tr>"); 203 out.println("</table>"); 204 out.println("</body>"); 205 out.println("</html>"); 206 } 207 finally 208 { 209 if (out != null) 210 { 211 out.close(); 212 } 213 } 214 } 215 216 private void generateOverviews() throws IOException 217 { 218 generateOverview(null); 219 Iterator iter = coverage.getPackages().iterator(); 220 while (iter.hasNext()) 221 { 222 Package pkg = (Package)iter.next(); 223 generateOverview(pkg); 224 } 225 } 226 227 private void generateOverview(Package pkg) throws IOException 228 { 229 String filename; 230 if (pkg == null) 231 { 232 filename = "frame-summary.html"; 233 } 234 else 235 { 236 filename = "frame-summary-" + pkg.getName() + ".html"; 237 } 238 File file = new File(outputDir, filename); 239 PrintStream out = null; 240 241 try 242 { 243 out = new PrintStream(new FileOutputStream(file)); 244 245 out.println("<html>"); 246 out.println("<head>"); 247 out.println("<title>Coverage Report</title>"); 248 out 249 .println("<link title=\"Style\" type=\"text/css\" rel=\"stylesheet\" href=\"css/main.css\" />"); 250 out 251 .println("<link title=\"Style\" type=\"text/css\" rel=\"stylesheet\" href=\"css/sortabletable.css\" />"); 252 out 253 .println("<script type=\"text/javascript\" src=\"js/popup.js\"></script>"); 254 out 255 .println("<script type=\"text/javascript\" src=\"js/sortabletable.js\"></script>"); 256 out 257 .println("<script type=\"text/javascript\" src=\"js/percentagesorttype.js\"></script>"); 258 out.println("</head>"); 259 out.println("<body>"); 260 261 out.print("<h5>Coverage Report - "); 262 out.print(pkg == null ? "All Packages" : pkg.getName()); 263 out.println("</h5>"); 264 out.println("<p>"); 265 out.println("<table class=\"report\" id=\"packageResults\">"); 266 out.println("<thead>"); 267 out.println("<tr>"); 268 out.println(" <td class=\"heading\">Package</td>"); 269 out.println(" <td class=\"heading\"># Classes</td>"); 270 out.println(generateCommonTableColumns()); 271 out.println("</tr>"); 272 out.println("</thead>"); 273 out.println("<tbody>"); 274 275 Set packages; 276 if (pkg == null) 277 { 278 // Output a summary line for all packages 279 out.println(generateTableRowForTotal()); 280 281 // Get packages 282 packages = coverage.getPackages(); 283 284 } 285 else 286 { 287 // Output a line for the current package 288 out.println(generateTableRowForPackage(pkg)); 289 290 // Get subpackages 291 packages = coverage.getSubPackages(pkg); 292 } 293 294 // Output a line for each package or subpackage 295 if (packages.size() > 0) 296 { 297 Iterator iter = packages.iterator(); 298 while (iter.hasNext()) 299 { 300 Package subpkg = (Package)iter.next(); 301 out.println(generateTableRowForPackage(subpkg)); 302 } 303 } 304 305 out.println("</tbody>"); 306 out.println("</table>"); 307 out.println("<script type=\"text/javascript\">"); 308 out 309 .println("var packageTable = new SortableTable(document.getElementById(\"packageResults\"),"); 310 out 311 .println(" [\"String\", \"Number\", \"Percentage\", \"Percentage\", \"Number\", \"Number\", \"Number\"]);"); 312 out.println("packageTable.sort(0);"); 313 out.println("</script>"); 314 out.println("</p>"); 315 316 // Get the list of classes in this package 317 Set classes; 318 if (pkg == null) 319 { 320 classes = new TreeSet(); 321 if (coverage.getClasses().size() > 0) 322 { 323 Iterator iter = coverage.getClasses().iterator(); 324 while (iter.hasNext()) 325 { 326 Clazz clazz = (Clazz)iter.next(); 327 if (clazz.getPackageName() == null) 328 { 329 classes.add(clazz); 330 } 331 } 332 } 333 } 334 else 335 { 336 classes = pkg.getClasses(); 337 } 338 339 // Output a line for each class 340 if (classes.size() > 0) 341 { 342 out.println("<p>"); 343 out.println("<table class=\"report\" id=\"classResults\">"); 344 out.println(generateTableHeaderForClasses()); 345 out.println("<tbody>"); 346 347 Iterator iter = classes.iterator(); 348 while (iter.hasNext()) 349 { 350 Clazz clazz = (Clazz)iter.next(); 351 out.println(generateTableRowForClass(clazz)); 352 } 353 354 out.println("</tbody>"); 355 out.println("</table>"); 356 out.println("<script type=\"text/javascript\">"); 357 out 358 .println("var classTable = new SortableTable(document.getElementById(\"classResults\"),"); 359 out 360 .println(" [\"String\", \"Percentage\", \"Percentage\", \"Number\", \"Number\", \"Number\"]);"); 361 out.println("classTable.sort(0);"); 362 out.println("</script>"); 363 out.println("</p>"); 364 } 365 366 out.println("<div class=\"footer\">"); 367 out 368 .println("Reports generated by <a href=\"http://cobertura.sourceforge.net/\" target=\"_top\">Cobertura</a>."); 369 out.println("</div>"); 370 371 out.println("</body>"); 372 out.println("</html>"); 373 } 374 finally 375 { 376 if (out != null) 377 { 378 out.close(); 379 } 380 } 381 } 382 383 private void generateSourceFiles() throws IOException 384 { 385 Iterator iter = coverage.getClasses().iterator(); 386 while (iter.hasNext()) 387 { 388 Clazz clazz = (Clazz)iter.next(); 389 generateSourceFile(clazz); 390 } 391 } 392 393 private void generateSourceFile(Clazz clazz) throws IOException 394 { 395 String filename = clazz.getLongName() + ".html"; 396 File file = new File(outputDir, filename); 397 PrintStream out = null; 398 399 try 400 { 401 out = new PrintStream(new FileOutputStream(file)); 402 403 out.println("<html>"); 404 out.println("<head>"); 405 out.println("<title>Coverage Report</title>"); 406 out 407 .println("<link title=\"Style\" type=\"text/css\" rel=\"stylesheet\" href=\"css/main.css\" />"); 408 out 409 .println("<script type=\"text/javascript\" src=\"js/popup.js\"></script>"); 410 out.println("</head>"); 411 out.println("<body>"); 412 out.print("<h5>Coverage Report - "); 413 if (clazz.getPackageName() != null) 414 { 415 out.print(clazz.getPackageName() + "."); 416 } 417 out.print(clazz.getName()); 418 out.println("</h5>"); 419 420 // Output the coverage summary for this class 421 out.println("<p>"); 422 out.println("<table class=\"report\">"); 423 out.println(generateTableHeaderForClasses()); 424 out.println(generateTableRowForClass(clazz)); 425 out.println("</table>"); 426 out.println("</p>"); 427 428 // Output this class's source code with syntax and coverage highlighting 429 out.println("<p>"); 430 out 431 .println("<table cellspacing=\"0\" cellpadding=\"0\" class=\"src\">"); 432 BufferedReader br = null; 433 try 434 { 435 br = new BufferedReader(new FileReader(new File(sourceDir, 436 clazz.getLongFileName()))); 437 String lineStr; 438 JavaToHtml javaToHtml = new JavaToHtml(); 439 int lineNumber = 1; 440 while ((lineStr = br.readLine()) != null) 441 { 442 out.println("<tr>"); 443 if (clazz.isValidSourceLine(lineNumber)) 444 { 445 long numberOfHits = clazz.getNumberOfHits(lineNumber); 446 out.println(" <td class=\"numLineCover\"> " 447 + lineNumber + "</td>"); 448 if (numberOfHits > 0) 449 { 450 out 451 .println(" <td class=\"nbHitsCovered\"> " 452 + numberOfHits + "</td>"); 453 out 454 .println(" <td class=\"src\"><pre class=\"src\"> " 455 + javaToHtml.process(lineStr) 456 + "</pre></td>"); 457 } 458 else 459 { 460 out 461 .println(" <td class=\"nbHitsUncovered\"> " 462 + numberOfHits + "</td>"); 463 out 464 .println(" <td class=\"src\"><pre class=\"src\"><span class=\"srcUncovered\"> " 465 + javaToHtml.process(lineStr) 466 + "</span></pre></td>"); 467 } 468 } 469 else 470 { 471 out.println(" <td class=\"numLine\"> " 472 + lineNumber + "</td>"); 473 out.println(" <td class=\"nbHits\"> </td>"); 474 out 475 .println(" <td class=\"src\"><pre class=\"src\"> " 476 + javaToHtml.process(lineStr) 477 + "</pre></td>"); 478 } 479 out.println("</tr>"); 480 lineNumber++; 481 } 482 } 483 finally 484 { 485 if (br != null) 486 { 487 br.close(); 488 } 489 } 490 out.println("</table>"); 491 out.println("</p>"); 492 493 out.println("<div class=\"footer\">"); 494 out 495 .println("Reports generated by <a href=\"http://cobertura.sourceforge.net/\" target=\"_top\">Cobertura</a>."); 496 out.println("</div>"); 497 498 out.println("</body>"); 499 out.println("</html>"); 500 } 501 finally 502 { 503 if (out != null) 504 { 505 out.close(); 506 } 507 } 508 } 509 510 private static String generateHelpURL(String text, String description) 511 { 512 StringBuffer ret = new StringBuffer(); 513 boolean popupTooltips = false; 514 if (popupTooltips) 515 { 516 ret 517 .append("<a class=\"hastooltip\" href=\"help.html\" onClick=\"popupwindow('help.html'); return false;\">"); 518 ret.append(text); 519 ret.append("<span>" + description + "</span>"); 520 ret.append("</a>"); 521 } 522 else 523 { 524 ret 525 .append("<a class=\"dfn\" href=\"help.html\" onClick=\"popupwindow('help.html'); return false;\">"); 526 ret.append(text); 527 ret.append("</a>"); 528 } 529 return ret.toString(); 530 } 531 532 private static String generateCommonTableColumns() 533 { 534 StringBuffer ret = new StringBuffer(); 535 ret.append(" <td class=\"heading\" width=\"20%\">" 536 + generateHelpURL("Line Coverage", 537 "The percent of lines executed by this test run.") 538 + "</td>"); 539 ret.append(" <td class=\"heading\" width=\"20%\">" 540 + generateHelpURL("Branch Coverage", 541 "The percent of branches executed by this test run.") 542 + "</td>"); 543 ret 544 .append(" <td class=\"heading\" width=\"10%\">" 545 + generateHelpURL( 546 "Complexity", 547 "Average McCabe's cyclomatic code complexity for all methods. This is basically a count of the number of different code paths in a method (incremented by 1 for each if statement, while loop, etc.)") 548 + "</td>"); 549 return ret.toString(); 550 } 551 552 private static String generateTableHeaderForClasses() 553 { 554 StringBuffer ret = new StringBuffer(); 555 ret.append("<thead>"); 556 ret.append("<tr>"); 557 ret.append(" <td class=\"heading\">Classes in this Package</td>"); 558 ret.append(generateCommonTableColumns()); 559 ret.append("</tr>"); 560 ret.append("</thead>"); 561 return ret.toString(); 562 } 563 564 private static String generateNAPercent() 565 { 566 StringBuffer sb = new StringBuffer(); 567 sb 568 .append("<table cellpadding=\"0\" cellspacing=\"0\" align=\"right\">"); 569 sb.append("<tr>"); 570 sb 571 .append("<td>" 572 + generateHelpURL( 573 "N/A", 574 "Line coverage and branch coverage will appear as \"Not Applicable\" when Cobertura can not find line number information in the .class file. This happens for stub and skeleton classes, interfaces, or when the class was not compiled with \"debug=true.\"") 575 + " </td>"); 576 sb.append("<td>"); 577 sb 578 .append("<table class=\"percentGraph\" cellpadding=\"0\" cellspacing=\"0\">"); 579 sb.append("<tr><td class=\"NA\" width=\"100\"></td></tr>"); 580 sb.append("</table>"); 581 sb.append("</td>"); 582 sb.append("</tr>"); 583 sb.append("</table>"); 584 return sb.toString(); 585 } 586 587 private static String generateTableColumnsForNA(double ccn) 588 { 589 return "<td class=\"value\">" + generateNAPercent() + "</td>" 590 + "<td class=\"value\">" + generateNAPercent() + "</td>" 591 + "<td class=\"value\">" + getDoubleValue(ccn) + "</td>"; 592 593 } 594 595 private static String generateTableColumnsFromData(double lineCoverage, 596 double branchCoverage, double ccn) 597 { 598 return "<td class=\"value\">" + generatePercentResult(lineCoverage) 599 + "</td>" + "<td class=\"value\">" 600 + generatePercentResult(branchCoverage) + "</td>" 601 + "<td class=\"value\">" + getDoubleValue(ccn) + "</td>"; 602 603 } 604 605 private String generateTableRowForTotal() 606 { 607 StringBuffer ret = new StringBuffer(); 608 double lineCoverage = coverage.getLineCoverageRate(); 609 double branchCoverage = coverage.getBranchCoverageRate(); 610 double ccn = Util.getCCN(sourceDir, true); 611 ret.append(" <tr>"); 612 ret.append("<td class=\"text\"><b>All Packages</b></td>"); 613 ret.append("<td class=\"value\">" + coverage.getNumberOfClasses() 614 + "</td>"); 615 ret.append(generateTableColumnsFromData(lineCoverage, branchCoverage, 616 ccn)); 617 ret.append("</tr>"); 618 return ret.toString(); 619 } 620 621 private String generateTableRowForPackage(Package pkg) 622 { 623 StringBuffer ret = new StringBuffer(); 624 String url1 = "frame-summary-" + pkg.getName() + ".html"; 625 String url2 = "frame-classes-" + pkg.getName() + ".html"; 626 double lineCoverage = pkg.getLineCoverageRate(); 627 double branchCoverage = pkg.getBranchCoverageRate(); 628 double ccn = Util.getCCN(new File(sourceDir, pkg.getFileName()), 629 false); 630 ret.append(" <tr>"); 631 ret.append("<td class=\"text\"><a href=\"" + url1 632 + "\" onClick='parent.classList.location.href=\"" + url2 633 + "\"'>" + pkg.getName() + "</a></td>"); 634 ret 635 .append("<td class=\"value\">" + pkg.getClasses().size() 636 + "</td>"); 637 ret.append(generateTableColumnsFromData(lineCoverage, branchCoverage, 638 ccn)); 639 ret.append("</tr>"); 640 return ret.toString(); 641 } 642 643 private String generateTableRowForClass(Clazz clazz) 644 { 645 StringBuffer ret = new StringBuffer(); 646 double lineCoverage = clazz.getLineCoverageRate(); 647 double branchCoverage = clazz.getBranchCoverageRate(); 648 double ccn = Util.getCCN( 649 new File(sourceDir, clazz.getLongFileName()), false); 650 ret.append(" <tr>"); 651 ret.append("<td class=\"text\"><a href=\"" + clazz.getLongName() 652 + ".html\">" + clazz.getName() + "</a></td>"); 653 if (clazz.getNumberOfLines() == 0) 654 { 655 ret.append(generateTableColumnsForNA(ccn)); 656 } 657 else 658 { 659 ret.append(generateTableColumnsFromData(lineCoverage, 660 branchCoverage, ccn)); 661 } 662 ret.append("</tr>"); 663 return ret.toString(); 664 } 665 666 private static String generatePercentResult(double percentValue) 667 { 668 double rest = 1d - percentValue; 669 StringBuffer sb = new StringBuffer(); 670 sb 671 .append("<table cellpadding=\"0\" cellspacing=\"0\" align=\"right\">"); 672 sb.append("<tr>"); 673 sb.append("<td>" + getPercentValue(percentValue) + " </td>"); 674 sb.append("<td>"); 675 sb 676 .append("<table class=\"percentGraph\" cellpadding=\"0\" cellspacing=\"0\">"); 677 sb.append("<tr>"); 678 sb.append("<td class=\"covered\" width=\"" 679 + (int)(percentValue * 100) + "\"></td>"); 680 sb.append("<td class=\"uncovered\" width=\"" + (int)(rest * 100) 681 + "\"></td>"); 682 sb.append("</tr>"); 683 sb.append("</table>"); 684 sb.append("</td>"); 685 sb.append("</tr>"); 686 sb.append("</table>"); 687 return sb.toString(); 688 } 689 690 private static String getDoubleValue(double value) 691 { 692 NumberFormat formatter; 693 formatter = new DecimalFormat(); 694 return formatter.format(value); 695 } 696 697 private static String getPercentValue(double value) 698 { 699 NumberFormat formatter; 700 formatter = NumberFormat.getPercentInstance(); 701 return formatter.format(value); 702 } 703 704 }