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