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 }