ReportTask.java
1/*******************************************************************************
2 * Copyright (c) 2009, 2010 Mountainminds GmbH & Co. KG and Contributors
3 * All rights reserved. This program and the accompanying materials
4 * are made available under the terms of the Eclipse Public License v1.0
5 * which accompanies this distribution, and is available at
6 * http://www.eclipse.org/legal/epl-v10.html
7 *
8 * Contributors:
9 * Marc R. Hoffmann - initial API and implementation
10 *
11 *******************************************************************************/
12package org.jacoco.ant;
13
14import static java.lang.String.format;
15
16import java.io.BufferedInputStream;
17import java.io.File;
18import java.io.FileOutputStream;
19import java.io.IOException;
20import java.io.InputStream;
21import java.io.InputStreamReader;
22import java.io.Reader;
23import java.util.ArrayList;
24import java.util.Collection;
25import java.util.HashMap;
26import java.util.Iterator;
27import java.util.List;
28import java.util.Locale;
29import java.util.Map;
30import java.util.zip.ZipOutputStream;
31
32import org.apache.tools.ant.BuildException;
33import org.apache.tools.ant.Project;
34import org.apache.tools.ant.Task;
35import org.apache.tools.ant.types.Resource;
36import org.apache.tools.ant.types.resources.FileResource;
37import org.apache.tools.ant.types.resources.Union;
38import org.apache.tools.ant.util.FileUtils;
39import org.jacoco.core.analysis.BundleCoverage;
40import org.jacoco.core.analysis.ClassCoverage;
41import org.jacoco.core.analysis.CoverageBuilder;
42import org.jacoco.core.analysis.CoverageNodeImpl;
43import org.jacoco.core.analysis.ICoverageNode;
44import org.jacoco.core.analysis.ICoverageNode.ElementType;
45import org.jacoco.core.analysis.PackageCoverage;
46import org.jacoco.core.data.ExecutionDataReader;
47import org.jacoco.core.data.ExecutionDataStore;
48import org.jacoco.core.data.SessionInfoStore;
49import org.jacoco.core.instr.Analyzer;
50import org.jacoco.report.FileMultiReportOutput;
51import org.jacoco.report.FileSingleReportOutput;
52import org.jacoco.report.IMultiReportOutput;
53import org.jacoco.report.IReportFormatter;
54import org.jacoco.report.IReportVisitor;
55import org.jacoco.report.ISourceFileLocator;
56import org.jacoco.report.MultiFormatter;
57import org.jacoco.report.ZipMultiReportOutput;
58import org.jacoco.report.csv.CSVFormatter;
59import org.jacoco.report.html.HTMLFormatter;
60import org.jacoco.report.xml.XMLFormatter;
61
62/**
63 * Task for coverage report generation. Experimental implementation that needs
64 * refinement.
65 *
66 * @author Marc R. Hoffmann
67 * @version 0.4.1.20101007204400
68 */
69public class ReportTask extends Task {
70
71 /**
72 * The source files are specified in a resource collection with additional
73 * attributes.
74 */
75 public static class SourceFilesElement extends Union {
76
77 String encoding;
78
79 /**
80 * Defines the optional source file encoding. If not set the platform
81 * default is used.
82 *
83 * @param encoding
84 * source file encoding
85 */
86 public void setEncoding(final String encoding) {
87 this.encoding = encoding;
88 }
89
90 }
91
92 /**
93 * Container element for class file groups.
94 */
95 public static class GroupElement {
96
97 private final List<GroupElement> children = new ArrayList<GroupElement>();
98
99 private final Union classfiles = new Union();
100
101 private final SourceFilesElement sourcefiles = new SourceFilesElement();
102
103 private String name;
104
105 /**
106 * Sets the name of the group.
107 *
108 * @param name
109 * name of the group
110 */
111 public void setName(final String name) {
112 this.name = name;
113 }
114
115 /**
116 * Creates a new child group.
117 *
118 * @return new child group
119 */
120 public GroupElement createGroup() {
121 final GroupElement group = new GroupElement();
122 children.add(group);
123 return group;
124 }
125
126 /**
127 * Returns the nested resource collection for class files.
128 *
129 * @return resource collection for class files
130 */
131 public Union createClassfiles() {
132 return classfiles;
133 }
134
135 /**
136 * Returns the nested resource collection for source files.
137 *
138 * @return resource collection for source files
139 */
140 public SourceFilesElement createSourcefiles() {
141 return sourcefiles;
142 }
143
144 }
145
146 /**
147 * Interface for child elements that define formatters.
148 */
149 private interface IFormatterElement {
150
151 IReportFormatter createFormatter() throws IOException;
152
153 void finish() throws IOException;
154
155 }
156
157 /**
158 * Formatter Element for HTML reports.
159 */
160 public static class HTMLFormatterElement implements IFormatterElement {
161
162 private File destdir;
163
164 private File destfile;
165
166 private String footer = "";
167
168 private String encoding = "UTF-8";
169
170 private Locale locale = Locale.getDefault();
171
172 private ZipOutputStream zipOutput;
173
174 /**
175 * Sets the output directory for the report.
176 *
177 * @param destdir
178 * output directory
179 */
180 public void setDestdir(final File destdir) {
181 this.destdir = destdir;
182 }
183
184 /**
185 * Sets the Zip output file for the report.
186 *
187 * @param destfile
188 * Zip output file
189 */
190 public void setDestfile(final File destfile) {
191 this.destfile = destfile;
192 }
193
194 /**
195 * Sets an optional footer text that will be displayed on every report
196 * page.
197 *
198 * @param text
199 * footer text
200 */
201 public void setFooter(final String text) {
202 this.footer = text;
203 }
204
205 /**
206 * Sets the output encoding for generated HTML files. Default is UTF-8.
207 *
208 * @param encoding
209 * output encoding
210 */
211 public void setEncoding(final String encoding) {
212 this.encoding = encoding;
213 }
214
215 /**
216 * Sets the locale for generated text output. By default the platform
217 * locale is used.
218 *
219 * @param locale
220 * text locale
221 */
222 public void setLocale(final Locale locale) {
223 this.locale = locale;
224 }
225
226 public IReportFormatter createFormatter() throws IOException {
227 final IMultiReportOutput output;
228 if (destfile != null) {
229 if (destdir != null) {
230 throw new BuildException(
231 "Either destination directory or file must be supplied, not both");
232 }
233 zipOutput = new ZipOutputStream(new FileOutputStream(destfile));
234 output = new ZipMultiReportOutput(zipOutput);
235
236 } else {
237 if (destdir == null) {
238 throw new BuildException(
239 "Destination directory or file must be supplied for html report");
240 }
241 output = new FileMultiReportOutput(destdir);
242 }
243 final HTMLFormatter formatter = new HTMLFormatter();
244 formatter.setReportOutput(output);
245 formatter.setFooterText(footer);
246 formatter.setOutputEncoding(encoding);
247 formatter.setLocale(locale);
248 return formatter;
249 }
250
251 public void finish() throws IOException {
252 if (zipOutput != null) {
253 zipOutput.close();
254 }
255 }
256
257 }
258
259 /**
260 * Formatter Element for CSV reports.
261 */
262 public static class CSVFormatterElement implements IFormatterElement {
263
264 private File destfile;
265
266 private String encoding = "UTF-8";
267
268 /**
269 * Sets the output file for the report.
270 *
271 * @param destfile
272 * output file
273 */
274 public void setDestfile(final File destfile) {
275 this.destfile = destfile;
276 }
277
278 public IReportFormatter createFormatter() {
279 if (destfile == null) {
280 throw new BuildException(
281 "Destination file must be supplied for csv report");
282 }
283 final CSVFormatter formatter = new CSVFormatter();
284 formatter.setReportOutput(new FileSingleReportOutput(destfile));
285 formatter.setOutputEncoding(encoding);
286 return formatter;
287 }
288
289 /**
290 * Sets the output encoding for generated XML file. Default is UTF-8.
291 *
292 * @param encoding
293 * output encoding
294 */
295 public void setEncoding(final String encoding) {
296 this.encoding = encoding;
297 }
298
299 public void finish() {
300 }
301
302 }
303
304 /**
305 * Formatter Element for XML reports.
306 */
307 public static class XMLFormatterElement implements IFormatterElement {
308
309 private File destfile;
310
311 private String encoding = "UTF-8";
312
313 /**
314 * Sets the output file for the report.
315 *
316 * @param destfile
317 * output file
318 */
319 public void setDestfile(final File destfile) {
320 this.destfile = destfile;
321 }
322
323 /**
324 * Sets the output encoding for generated XML file. Default is UTF-8.
325 *
326 * @param encoding
327 * output encoding
328 */
329 public void setEncoding(final String encoding) {
330 this.encoding = encoding;
331 }
332
333 public IReportFormatter createFormatter() {
334 if (destfile == null) {
335 throw new BuildException(
336 "Destination file must be supplied for xml report");
337 }
338 final XMLFormatter formatter = new XMLFormatter();
339 formatter.setReportOutput(new FileSingleReportOutput(destfile));
340 formatter.setOutputEncoding(encoding);
341 return formatter;
342 }
343
344 public void finish() {
345 }
346
347 }
348
349 private final Union executiondataElement = new Union();
350
351 private SessionInfoStore sessionInfoStore;
352
353 private ExecutionDataStore executionDataStore;
354
355 private final GroupElement structure = new GroupElement();
356
357 private final List<IFormatterElement> formatters = new ArrayList<IFormatterElement>();
358
359 /**
360 * Returns the nested resource collection for execution data files.
361 *
362 * @return resource collection for execution files
363 */
364 public Union createExecutiondata() {
365 return executiondataElement;
366 }
367
368 /**
369 * Returns the root group element that defines the report structure.
370 *
371 * @return root group element
372 */
373 public GroupElement createStructure() {
374 return structure;
375 }
376
377 /**
378 * Creates a new HTML report element.
379 *
380 * @return HTML report element
381 */
382 public HTMLFormatterElement createHtml() {
383 final HTMLFormatterElement element = new HTMLFormatterElement();
384 formatters.add(element);
385 return element;
386 }
387
388 /**
389 * Creates a new CSV report element.
390 *
391 * @return CSV report element
392 */
393 public CSVFormatterElement createCsv() {
394 final CSVFormatterElement element = new CSVFormatterElement();
395 formatters.add(element);
396 return element;
397 }
398
399 /**
400 * Creates a new XML report element.
401 *
402 * @return CSV report element
403 */
404 public XMLFormatterElement createXml() {
405 final XMLFormatterElement element = new XMLFormatterElement();
406 formatters.add(element);
407 return element;
408 }
409
410 @Override
411 public void execute() throws BuildException {
412 loadExecutionData();
413 try {
414 final IReportFormatter formatter = createFormatter();
415 createReport(formatter);
416 finishFormatters();
417 } catch (final IOException e) {
418 throw new BuildException("Error while creating report.", e);
419 }
420 }
421
422 private void loadExecutionData() {
423 sessionInfoStore = new SessionInfoStore();
424 executionDataStore = new ExecutionDataStore();
425 for (final Iterator<?> i = executiondataElement.iterator(); i.hasNext();) {
426 final Resource resource = (Resource) i.next();
427 InputStream in = null;
428 try {
429 in = new BufferedInputStream(resource.getInputStream());
430 final ExecutionDataReader reader = new ExecutionDataReader(in);
431 reader.setSessionInfoVisitor(sessionInfoStore);
432 reader.setExecutionDataVisitor(executionDataStore);
433 reader.read();
434 } catch (final IOException e) {
435 throw new BuildException("Unable to read execution data file "
436 + resource.getName(), e);
437 } finally {
438 FileUtils.close(in);
439 }
440 }
441 }
442
443 private IReportFormatter createFormatter() throws IOException {
444 final MultiFormatter multi = new MultiFormatter();
445 for (final IFormatterElement f : formatters) {
446 multi.add(f.createFormatter());
447 }
448 return multi;
449 }
450
451 private void finishFormatters() throws IOException {
452 for (final IFormatterElement f : formatters) {
453 f.finish();
454 }
455 }
456
457 private void createReport(final IReportFormatter formatter)
458 throws IOException {
459 final CoverageNodeImpl node = createNode(structure);
460 final IReportVisitor visitor = formatter.createReportVisitor(node,
461 sessionInfoStore.getInfos(), executionDataStore.getContents());
462 final SourceFileCollection sourceFileLocator = new SourceFileCollection(
463 structure.sourcefiles);
464 if (node instanceof BundleCoverage) {
465 visitBundle(visitor, (BundleCoverage) node, sourceFileLocator);
466 } else {
467 for (final GroupElement g : structure.children) {
468 createReport(g, visitor, node);
469 }
470 }
471 visitor.visitEnd(sourceFileLocator);
472 }
473
474 private void createReport(final GroupElement group,
475 final IReportVisitor parentVisitor,
476 final CoverageNodeImpl parentNode) throws IOException {
477 final CoverageNodeImpl node = createNode(group);
478 final IReportVisitor visitor = parentVisitor.visitChild(node);
479 final SourceFileCollection sourceFileLocator = new SourceFileCollection(
480 group.sourcefiles);
481 if (node instanceof BundleCoverage) {
482 visitBundle(visitor, (BundleCoverage) node, sourceFileLocator);
483 } else {
484 for (final GroupElement g : group.children) {
485 createReport(g, visitor, node);
486 }
487 }
488 parentNode.increment(node);
489 visitor.visitEnd(sourceFileLocator);
490 }
491
492 private CoverageNodeImpl createNode(final GroupElement group)
493 throws IOException {
494 if (group.name == null) {
495 throw new BuildException("Group name must be supplied");
496 }
497 if (group.children.size() > 0) {
498 return new CoverageNodeImpl(ElementType.GROUP, group.name, false);
499 } else {
500 final CoverageBuilder builder = new CoverageBuilder(
501 executionDataStore);
502 final Analyzer analyzer = new Analyzer(builder);
503 for (final Iterator<?> i = group.classfiles.iterator(); i.hasNext();) {
504 final Resource resource = (Resource) i.next();
505 if (resource.isDirectory() && resource instanceof FileResource) {
506 analyzer.analyzeAll(((FileResource) resource).getFile());
507 } else {
508 final InputStream in = resource.getInputStream();
509 analyzer.analyzeAll(in);
510 in.close();
511 }
512 }
513 return builder.getBundle(group.name);
514 }
515 }
516
517 private static class SourceFileCollection implements ISourceFileLocator {
518
519 private final String encoding;
520
521 private final Map<String, Resource> resources = new HashMap<String, Resource>();
522
523 SourceFileCollection(final SourceFilesElement sourceFiles) {
524 encoding = sourceFiles.encoding;
525 for (final Iterator<?> i = sourceFiles.iterator(); i.hasNext();) {
526 final Resource r = (Resource) i.next();
527 resources.put(r.getName().replace(File.separatorChar, '/'), r);
528 }
529 }
530
531 public Reader getSourceFile(final String packageName,
532 final String fileName) throws IOException {
533 final Resource r = resources.get(packageName + '/' + fileName);
534 if (r == null) {
535 return null;
536 }
537 if (encoding == null) {
538 return new InputStreamReader(r.getInputStream());
539 } else {
540 return new InputStreamReader(r.getInputStream(), encoding);
541 }
542 }
543
544 public boolean isEmpty() {
545 return resources.isEmpty();
546 }
547 }
548
549 private void visitBundle(final IReportVisitor visitor,
550 final BundleCoverage bundledata,
551 final SourceFileCollection sourceFileLocator) throws IOException {
552 if (!sourceFileLocator.isEmpty()) {
553 checkForMissingDebugInformation(bundledata);
554 }
555 for (final PackageCoverage p : bundledata.getPackages()) {
556 visitPackage(visitor.visitChild(p), p, sourceFileLocator);
557 }
558 }
559
560 private void checkForMissingDebugInformation(final ICoverageNode node) {
561 if (node.getClassCounter().getTotalCount() > 0
562 && node.getLineCounter().getTotalCount() == 0) {
563 log(format(
564 "To enable source code annotation class files for bundle '%s' have to be compiled with debug information.",
565 node.getName()), Project.MSG_WARN);
566 }
567 }
568
569 private static void visitPackage(final IReportVisitor visitor,
570 final PackageCoverage packagedata,
571 final ISourceFileLocator sourceFileLocator) throws IOException {
572 visitLeafs(visitor, packagedata.getSourceFiles(), sourceFileLocator);
573 for (final ClassCoverage c : packagedata.getClasses()) {
574 visitClass(visitor.visitChild(c), c, sourceFileLocator);
575 }
576 visitor.visitEnd(sourceFileLocator);
577 }
578
579 private static void visitClass(final IReportVisitor visitor,
580 final ClassCoverage classdata,
581 final ISourceFileLocator sourceFileLocator) throws IOException {
582 visitLeafs(visitor, classdata.getMethods(), sourceFileLocator);
583 visitor.visitEnd(sourceFileLocator);
584 }
585
586 private static void visitLeafs(final IReportVisitor visitor,
587 final Collection<? extends ICoverageNode> leafs,
588 final ISourceFileLocator sourceFileLocator) throws IOException {
589 for (final ICoverageNode l : leafs) {
590 final IReportVisitor child = visitor.visitChild(l);
591 child.visitEnd(sourceFileLocator);
592 }
593 }
594
595}