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