1 /*******************************************************************************
2  * Copyright (c) 2009, 2015 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  *******************************************************************************/
12 package org.jacoco.ant;
13 
14 import static java.lang.String.format;
15 
16 import java.io.File;
17 import java.io.FileOutputStream;
18 import java.io.IOException;
19 import java.io.InputStream;
20 import java.util.ArrayList;
21 import java.util.Collection;
22 import java.util.Iterator;
23 import java.util.List;
24 import java.util.Locale;
25 import java.util.StringTokenizer;
26 
27 import org.apache.tools.ant.BuildException;
28 import org.apache.tools.ant.Project;
29 import org.apache.tools.ant.Task;
30 import org.apache.tools.ant.types.Resource;
31 import org.apache.tools.ant.types.resources.FileResource;
32 import org.apache.tools.ant.types.resources.Union;
33 import org.apache.tools.ant.util.FileUtils;
34 import org.jacoco.core.analysis.Analyzer;
35 import org.jacoco.core.analysis.CoverageBuilder;
36 import org.jacoco.core.analysis.IBundleCoverage;
37 import org.jacoco.core.analysis.IClassCoverage;
38 import org.jacoco.core.analysis.ICoverageNode;
39 import org.jacoco.core.data.ExecutionDataStore;
40 import org.jacoco.core.data.SessionInfoStore;
41 import org.jacoco.core.tools.ExecFileLoader;
42 import org.jacoco.report.FileMultiReportOutput;
43 import org.jacoco.report.IMultiReportOutput;
44 import org.jacoco.report.IReportGroupVisitor;
45 import org.jacoco.report.IReportVisitor;
46 import org.jacoco.report.MultiReportVisitor;
47 import org.jacoco.report.ZipMultiReportOutput;
48 import org.jacoco.report.check.IViolationsOutput;
49 import org.jacoco.report.check.Limit;
50 import org.jacoco.report.check.Rule;
51 import org.jacoco.report.check.RulesChecker;
52 import org.jacoco.report.csv.CSVFormatter;
53 import org.jacoco.report.html.HTMLFormatter;
54 import org.jacoco.report.xml.XMLFormatter;
55 
56 /**
57  * Task for coverage report generation.
58  */
59 public class ReportTask extends Task {
60 
61 	/**
62 	 * The source files are specified in a resource collection with additional
63 	 * attributes.
64 	 */
65 	public static class SourceFilesElement extends Union {
66 
67 		String encoding = null;
68 
69 		int tabWidth = 4;
70 
71 		/**
72 		 * Defines the optional source file encoding. If not set the platform
73 		 * default is used.
74 		 *
75 		 * @param encoding
76 		 *            source file encoding
77 		 */
setEncoding(final String encoding)78 		public void setEncoding(final String encoding) {
79 			this.encoding = encoding;
80 		}
81 
82 		/**
83 		 * Sets the tab stop width for the source pages. Default value is 4.
84 		 *
85 		 * @param tabWidth
86 		 *            number of characters per tab stop
87 		 */
setTabwidth(final int tabWidth)88 		public void setTabwidth(final int tabWidth) {
89 			if (tabWidth <= 0) {
90 				throw new BuildException("Tab width must be greater than 0");
91 			}
92 			this.tabWidth = tabWidth;
93 		}
94 
95 	}
96 
97 	/**
98 	 * Container element for class file groups.
99 	 */
100 	public static class GroupElement {
101 
102 		private final List<GroupElement> children = new ArrayList<GroupElement>();
103 
104 		private final Union classfiles = new Union();
105 
106 		private final SourceFilesElement sourcefiles = new SourceFilesElement();
107 
108 		private String name;
109 
110 		/**
111 		 * Sets the name of the group.
112 		 *
113 		 * @param name
114 		 *            name of the group
115 		 */
setName(final String name)116 		public void setName(final String name) {
117 			this.name = name;
118 		}
119 
120 		/**
121 		 * Creates a new child group.
122 		 *
123 		 * @return new child group
124 		 */
createGroup()125 		public GroupElement createGroup() {
126 			final GroupElement group = new GroupElement();
127 			children.add(group);
128 			return group;
129 		}
130 
131 		/**
132 		 * Returns the nested resource collection for class files.
133 		 *
134 		 * @return resource collection for class files
135 		 */
createClassfiles()136 		public Union createClassfiles() {
137 			return classfiles;
138 		}
139 
140 		/**
141 		 * Returns the nested resource collection for source files.
142 		 *
143 		 * @return resource collection for source files
144 		 */
createSourcefiles()145 		public SourceFilesElement createSourcefiles() {
146 			return sourcefiles;
147 		}
148 
149 	}
150 
151 	/**
152 	 * Interface for child elements that define formatters.
153 	 */
154 	private abstract class FormatterElement {
155 
createVisitor()156 		abstract IReportVisitor createVisitor() throws IOException;
157 
finish()158 		void finish() {
159 		}
160 	}
161 
162 	/**
163 	 * Formatter element for HTML reports.
164 	 */
165 	public class HTMLFormatterElement extends FormatterElement {
166 
167 		private File destdir;
168 
169 		private File destfile;
170 
171 		private String footer = "";
172 
173 		private String encoding = "UTF-8";
174 
175 		private Locale locale = Locale.getDefault();
176 
177 		/**
178 		 * Sets the output directory for the report.
179 		 *
180 		 * @param destdir
181 		 *            output directory
182 		 */
setDestdir(final File destdir)183 		public void setDestdir(final File destdir) {
184 			this.destdir = destdir;
185 		}
186 
187 		/**
188 		 * Sets the Zip output file for the report.
189 		 *
190 		 * @param destfile
191 		 *            Zip output file
192 		 */
setDestfile(final File destfile)193 		public void setDestfile(final File destfile) {
194 			this.destfile = destfile;
195 		}
196 
197 		/**
198 		 * Sets an optional footer text that will be displayed on every report
199 		 * page.
200 		 *
201 		 * @param text
202 		 *            footer text
203 		 */
setFooter(final String text)204 		public void setFooter(final String text) {
205 			this.footer = text;
206 		}
207 
208 		/**
209 		 * Sets the output encoding for generated HTML files. Default is UTF-8.
210 		 *
211 		 * @param encoding
212 		 *            output encoding
213 		 */
setEncoding(final String encoding)214 		public void setEncoding(final String encoding) {
215 			this.encoding = encoding;
216 		}
217 
218 		/**
219 		 * Sets the locale for generated text output. By default the platform
220 		 * locale is used.
221 		 *
222 		 * @param locale
223 		 *            text locale
224 		 */
setLocale(final String locale)225 		public void setLocale(final String locale) {
226 			this.locale = parseLocale(locale);
227 		}
228 
229 		@Override
createVisitor()230 		public IReportVisitor createVisitor() throws IOException {
231 			final IMultiReportOutput output;
232 			if (destfile != null) {
233 				if (destdir != null) {
234 					throw new BuildException(
235 							"Either destination directory or file must be supplied, not both",
236 							getLocation());
237 				}
238 				final FileOutputStream stream = new FileOutputStream(destfile);
239 				output = new ZipMultiReportOutput(stream);
240 
241 			} else {
242 				if (destdir == null) {
243 					throw new BuildException(
244 							"Destination directory or file must be supplied for html report",
245 							getLocation());
246 				}
247 				output = new FileMultiReportOutput(destdir);
248 			}
249 			final HTMLFormatter formatter = new HTMLFormatter();
250 			formatter.setFooterText(footer);
251 			formatter.setOutputEncoding(encoding);
252 			formatter.setLocale(locale);
253 			return formatter.createVisitor(output);
254 		}
255 
256 	}
257 
258 	/**
259 	 * Formatter element for CSV reports.
260 	 */
261 	public class CSVFormatterElement extends FormatterElement {
262 
263 		private File destfile;
264 
265 		private String encoding = "UTF-8";
266 
267 		/**
268 		 * Sets the output file for the report.
269 		 *
270 		 * @param destfile
271 		 *            output file
272 		 */
setDestfile(final File destfile)273 		public void setDestfile(final File destfile) {
274 			this.destfile = destfile;
275 		}
276 
277 		@Override
createVisitor()278 		public IReportVisitor createVisitor() throws IOException {
279 			if (destfile == null) {
280 				throw new BuildException(
281 						"Destination file must be supplied for csv report",
282 						getLocation());
283 			}
284 			final CSVFormatter formatter = new CSVFormatter();
285 			formatter.setOutputEncoding(encoding);
286 			return formatter.createVisitor(new FileOutputStream(destfile));
287 		}
288 
289 		/**
290 		 * Sets the output encoding for generated XML file. Default is UTF-8.
291 		 *
292 		 * @param encoding
293 		 *            output encoding
294 		 */
setEncoding(final String encoding)295 		public void setEncoding(final String encoding) {
296 			this.encoding = encoding;
297 		}
298 
299 	}
300 
301 	/**
302 	 * Formatter element for XML reports.
303 	 */
304 	public class XMLFormatterElement extends FormatterElement {
305 
306 		private File destfile;
307 
308 		private String encoding = "UTF-8";
309 
310 		/**
311 		 * Sets the output file for the report.
312 		 *
313 		 * @param destfile
314 		 *            output file
315 		 */
setDestfile(final File destfile)316 		public void setDestfile(final File destfile) {
317 			this.destfile = destfile;
318 		}
319 
320 		/**
321 		 * Sets the output encoding for generated XML file. Default is UTF-8.
322 		 *
323 		 * @param encoding
324 		 *            output encoding
325 		 */
setEncoding(final String encoding)326 		public void setEncoding(final String encoding) {
327 			this.encoding = encoding;
328 		}
329 
330 		@Override
createVisitor()331 		public IReportVisitor createVisitor() throws IOException {
332 			if (destfile == null) {
333 				throw new BuildException(
334 						"Destination file must be supplied for xml report",
335 						getLocation());
336 			}
337 			final XMLFormatter formatter = new XMLFormatter();
338 			formatter.setOutputEncoding(encoding);
339 			return formatter.createVisitor(new FileOutputStream(destfile));
340 		}
341 
342 	}
343 
344 	/**
345 	 * Formatter element for coverage checks.
346 	 */
347 	public class CheckFormatterElement extends FormatterElement implements
348 			IViolationsOutput {
349 
350 		private final List<Rule> rules = new ArrayList<Rule>();
351 		private boolean violations = false;
352 		private boolean failOnViolation = true;
353 		private String violationsPropery = null;
354 
355 		/**
356 		 * Creates and adds a new rule.
357 		 *
358 		 * @return new rule
359 		 */
createRule()360 		public Rule createRule() {
361 			final Rule rule = new Rule();
362 			rules.add(rule);
363 			return rule;
364 		}
365 
366 		/**
367 		 * Sets whether the build should fail in case of a violation. Default is
368 		 * <code>true</code>.
369 		 *
370 		 * @param flag
371 		 *            if <code>true</code> the build fails on violation
372 		 */
setFailOnViolation(final boolean flag)373 		public void setFailOnViolation(final boolean flag) {
374 			this.failOnViolation = flag;
375 		}
376 
377 		/**
378 		 * Sets the name of a property to append the violation messages to.
379 		 *
380 		 * @param property
381 		 *            name of a property
382 		 */
setViolationsProperty(final String property)383 		public void setViolationsProperty(final String property) {
384 			this.violationsPropery = property;
385 		}
386 
387 		@Override
createVisitor()388 		public IReportVisitor createVisitor() throws IOException {
389 			final RulesChecker formatter = new RulesChecker();
390 			formatter.setRules(rules);
391 			return formatter.createVisitor(this);
392 		}
393 
onViolation(final ICoverageNode node, final Rule rule, final Limit limit, final String message)394 		public void onViolation(final ICoverageNode node, final Rule rule,
395 				final Limit limit, final String message) {
396 			log(message, Project.MSG_ERR);
397 			violations = true;
398 			if (violationsPropery != null) {
399 				final String old = getProject().getProperty(violationsPropery);
400 				final String value = old == null ? message : String.format(
401 						"%s\n%s", old, message);
402 				getProject().setProperty(violationsPropery, value);
403 			}
404 		}
405 
406 		@Override
finish()407 		void finish() {
408 			if (violations && failOnViolation) {
409 				throw new BuildException(
410 						"Coverage check failed due to violated rules.",
411 						getLocation());
412 			}
413 		}
414 	}
415 
416 	private final Union executiondataElement = new Union();
417 
418 	private SessionInfoStore sessionInfoStore;
419 
420 	private ExecutionDataStore executionDataStore;
421 
422 	private final GroupElement structure = new GroupElement();
423 
424 	private final List<FormatterElement> formatters = new ArrayList<FormatterElement>();
425 
426 	/**
427 	 * Returns the nested resource collection for execution data files.
428 	 *
429 	 * @return resource collection for execution files
430 	 */
createExecutiondata()431 	public Union createExecutiondata() {
432 		return executiondataElement;
433 	}
434 
435 	/**
436 	 * Returns the root group element that defines the report structure.
437 	 *
438 	 * @return root group element
439 	 */
createStructure()440 	public GroupElement createStructure() {
441 		return structure;
442 	}
443 
444 	/**
445 	 * Creates a new HTML report element.
446 	 *
447 	 * @return HTML report element
448 	 */
createHtml()449 	public HTMLFormatterElement createHtml() {
450 		final HTMLFormatterElement element = new HTMLFormatterElement();
451 		formatters.add(element);
452 		return element;
453 	}
454 
455 	/**
456 	 * Creates a new CSV report element.
457 	 *
458 	 * @return CSV report element
459 	 */
createCsv()460 	public CSVFormatterElement createCsv() {
461 		final CSVFormatterElement element = new CSVFormatterElement();
462 		formatters.add(element);
463 		return element;
464 	}
465 
466 	/**
467 	 * Creates a new coverage check element.
468 	 *
469 	 * @return coverage check element
470 	 */
createCheck()471 	public CheckFormatterElement createCheck() {
472 		final CheckFormatterElement element = new CheckFormatterElement();
473 		formatters.add(element);
474 		return element;
475 	}
476 
477 	/**
478 	 * Creates a new XML report element.
479 	 *
480 	 * @return CSV report element
481 	 */
createXml()482 	public XMLFormatterElement createXml() {
483 		final XMLFormatterElement element = new XMLFormatterElement();
484 		formatters.add(element);
485 		return element;
486 	}
487 
488 	@Override
execute()489 	public void execute() throws BuildException {
490 		loadExecutionData();
491 		try {
492 			final IReportVisitor visitor = createVisitor();
493 			visitor.visitInfo(sessionInfoStore.getInfos(),
494 					executionDataStore.getContents());
495 			createReport(visitor, structure);
496 			visitor.visitEnd();
497 			for (final FormatterElement f : formatters) {
498 				f.finish();
499 			}
500 		} catch (final IOException e) {
501 			throw new BuildException("Error while creating report", e,
502 					getLocation());
503 		}
504 	}
505 
loadExecutionData()506 	private void loadExecutionData() {
507 		final ExecFileLoader loader = new ExecFileLoader();
508 		for (final Iterator<?> i = executiondataElement.iterator(); i.hasNext();) {
509 			final Resource resource = (Resource) i.next();
510 			log(format("Loading execution data file %s", resource));
511 			InputStream in = null;
512 			try {
513 				in = resource.getInputStream();
514 				loader.load(in);
515 			} catch (final IOException e) {
516 				throw new BuildException(format(
517 						"Unable to read execution data file %s", resource), e,
518 						getLocation());
519 			} finally {
520 				FileUtils.close(in);
521 			}
522 		}
523 		sessionInfoStore = loader.getSessionInfoStore();
524 		executionDataStore = loader.getExecutionDataStore();
525 	}
526 
createVisitor()527 	private IReportVisitor createVisitor() throws IOException {
528 		final List<IReportVisitor> visitors = new ArrayList<IReportVisitor>();
529 		for (final FormatterElement f : formatters) {
530 			visitors.add(f.createVisitor());
531 		}
532 		return new MultiReportVisitor(visitors);
533 	}
534 
createReport(final IReportGroupVisitor visitor, final GroupElement group)535 	private void createReport(final IReportGroupVisitor visitor,
536 			final GroupElement group) throws IOException {
537 		if (group.name == null) {
538 			throw new BuildException("Group name must be supplied",
539 					getLocation());
540 		}
541 		if (group.children.isEmpty()) {
542 			final IBundleCoverage bundle = createBundle(group);
543 			final SourceFilesElement sourcefiles = group.sourcefiles;
544 			final AntResourcesLocator locator = new AntResourcesLocator(
545 					sourcefiles.encoding, sourcefiles.tabWidth);
546 			locator.addAll(sourcefiles.iterator());
547 			if (!locator.isEmpty()) {
548 				checkForMissingDebugInformation(bundle);
549 			}
550 			visitor.visitBundle(bundle, locator);
551 		} else {
552 			final IReportGroupVisitor groupVisitor = visitor
553 					.visitGroup(group.name);
554 			for (final GroupElement child : group.children) {
555 				createReport(groupVisitor, child);
556 			}
557 		}
558 	}
559 
createBundle(final GroupElement group)560 	private IBundleCoverage createBundle(final GroupElement group)
561 			throws IOException {
562 		final CoverageBuilder builder = new CoverageBuilder();
563 		final Analyzer analyzer = new Analyzer(executionDataStore, builder);
564 		for (final Iterator<?> i = group.classfiles.iterator(); i.hasNext();) {
565 			final Resource resource = (Resource) i.next();
566 			if (resource.isDirectory() && resource instanceof FileResource) {
567 				analyzer.analyzeAll(((FileResource) resource).getFile());
568 			} else {
569 				final InputStream in = resource.getInputStream();
570 				analyzer.analyzeAll(in, resource.getName());
571 				in.close();
572 			}
573 		}
574 		final IBundleCoverage bundle = builder.getBundle(group.name);
575 		logBundleInfo(bundle, builder.getNoMatchClasses());
576 		return bundle;
577 	}
578 
logBundleInfo(final IBundleCoverage bundle, final Collection<IClassCoverage> nomatch)579 	private void logBundleInfo(final IBundleCoverage bundle,
580 			final Collection<IClassCoverage> nomatch) {
581 		log(format("Writing bundle '%s' with %s classes", bundle.getName(),
582 				Integer.valueOf(bundle.getClassCounter().getTotalCount())));
583 		if (!nomatch.isEmpty()) {
584 			log(format(
585 					"Classes in bundle '%s' do no match with execution data. "
586 							+ "For report generation the same class files must be used as at runtime.",
587 					bundle.getName()), Project.MSG_WARN);
588 			for (final IClassCoverage c : nomatch) {
589 				log(format("Execution data for class %s does not match.",
590 						c.getName()), Project.MSG_WARN);
591 			}
592 		}
593 	}
594 
checkForMissingDebugInformation(final ICoverageNode node)595 	private void checkForMissingDebugInformation(final ICoverageNode node) {
596 		if (node.getClassCounter().getTotalCount() > 0
597 				&& node.getLineCounter().getTotalCount() == 0) {
598 			log(format(
599 					"To enable source code annotation class files for bundle '%s' have to be compiled with debug information.",
600 					node.getName()), Project.MSG_WARN);
601 		}
602 	}
603 
604 	/**
605 	 * Splits a given underscore "_" separated string and creates a Locale. This
606 	 * method is implemented as the method Locale.forLanguageTag() was not
607 	 * available in Java 5.
608 	 *
609 	 * @param locale
610 	 *            String representation of a Locate
611 	 * @return Locale instance
612 	 */
parseLocale(final String locale)613 	static Locale parseLocale(final String locale) {
614 		final StringTokenizer st = new StringTokenizer(locale, "_");
615 		final String language = st.hasMoreTokens() ? st.nextToken() : "";
616 		final String country = st.hasMoreTokens() ? st.nextToken() : "";
617 		final String variant = st.hasMoreTokens() ? st.nextToken() : "";
618 		return new Locale(language, country, variant);
619 	}
620 
621 }
622