/*******************************************************************************
* Copyright (c) 2009, 2014 Mountainminds GmbH & Co. KG and Contributors
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Marc R. Hoffmann - initial API and implementation
*
*******************************************************************************/
package org.jacoco.ant;
import static java.lang.String.format;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.Task;
import org.apache.tools.ant.types.Resource;
import org.apache.tools.ant.types.resources.FileResource;
import org.apache.tools.ant.types.resources.Union;
import org.apache.tools.ant.util.FileUtils;
import org.jacoco.core.analysis.Analyzer;
import org.jacoco.core.analysis.CoverageBuilder;
import org.jacoco.core.analysis.IBundleCoverage;
import org.jacoco.core.analysis.IClassCoverage;
import org.jacoco.core.analysis.ICoverageNode;
import org.jacoco.core.data.ExecutionDataStore;
import org.jacoco.core.data.SessionInfoStore;
import org.jacoco.core.tools.ExecFileLoader;
import org.jacoco.report.FileMultiReportOutput;
import org.jacoco.report.IMultiReportOutput;
import org.jacoco.report.IReportGroupVisitor;
import org.jacoco.report.IReportVisitor;
import org.jacoco.report.MultiReportVisitor;
import org.jacoco.report.ZipMultiReportOutput;
import org.jacoco.report.check.IViolationsOutput;
import org.jacoco.report.check.Limit;
import org.jacoco.report.check.Rule;
import org.jacoco.report.check.RulesChecker;
import org.jacoco.report.csv.CSVFormatter;
import org.jacoco.report.html.HTMLFormatter;
import org.jacoco.report.xml.XMLFormatter;
/**
* Task for coverage report generation.
*/
public class ReportTask extends Task {
/**
* The source files are specified in a resource collection with additional
* attributes.
*/
public static class SourceFilesElement extends Union {
String encoding = null;
int tabWidth = 4;
/**
* Defines the optional source file encoding. If not set the platform
* default is used.
*
* @param encoding
* source file encoding
*/
public void setEncoding(final String encoding) {
this.encoding = encoding;
}
/**
* Sets the tab stop width for the source pages. Default value is 4.
*
* @param tabWidth
* number of characters per tab stop
*/
public void setTabwidth(final int tabWidth) {
if (tabWidth <= 0) {
throw new BuildException("Tab width must be greater than 0");
}
this.tabWidth = tabWidth;
}
}
/**
* Container element for class file groups.
*/
public static class GroupElement {
private final List<GroupElement> children = new ArrayList<GroupElement>();
private final Union classfiles = new Union();
private final SourceFilesElement sourcefiles = new SourceFilesElement();
private String name;
/**
* Sets the name of the group.
*
* @param name
* name of the group
*/
public void setName(final String name) {
this.name = name;
}
/**
* Creates a new child group.
*
* @return new child group
*/
public GroupElement createGroup() {
final GroupElement group = new GroupElement();
children.add(group);
return group;
}
/**
* Returns the nested resource collection for class files.
*
* @return resource collection for class files
*/
public Union createClassfiles() {
return classfiles;
}
/**
* Returns the nested resource collection for source files.
*
* @return resource collection for source files
*/
public SourceFilesElement createSourcefiles() {
return sourcefiles;
}
}
/**
* Interface for child elements that define formatters.
*/
private abstract class FormatterElement {
abstract IReportVisitor createVisitor() throws IOException;
void finish() {
}
}
/**
* Formatter element for HTML reports.
*/
public class HTMLFormatterElement extends FormatterElement {
private File destdir;
private File destfile;
private String footer = "";
private String encoding = "UTF-8";
private Locale locale = Locale.getDefault();
/**
* Sets the output directory for the report.
*
* @param destdir
* output directory
*/
public void setDestdir(final File destdir) {
this.destdir = destdir;
}
/**
* Sets the Zip output file for the report.
*
* @param destfile
* Zip output file
*/
public void setDestfile(final File destfile) {
this.destfile = destfile;
}
/**
* Sets an optional footer text that will be displayed on every report
* page.
*
* @param text
* footer text
*/
public void setFooter(final String text) {
this.footer = text;
}
/**
* Sets the output encoding for generated HTML files. Default is UTF-8.
*
* @param encoding
* output encoding
*/
public void setEncoding(final String encoding) {
this.encoding = encoding;
}
/**
* Sets the locale for generated text output. By default the platform
* locale is used.
*
* @param locale
* text locale
*/
public void setLocale(final Locale locale) {
this.locale = locale;
}
@Override
public IReportVisitor createVisitor() throws IOException {
final IMultiReportOutput output;
if (destfile != null) {
if (destdir != null) {
throw new BuildException(
"Either destination directory or file must be supplied, not both",
getLocation());
}
final FileOutputStream stream = new FileOutputStream(destfile);
output = new ZipMultiReportOutput(stream);
} else {
if (destdir == null) {
throw new BuildException(
"Destination directory or file must be supplied for html report",
getLocation());
}
output = new FileMultiReportOutput(destdir);
}
final HTMLFormatter formatter = new HTMLFormatter();
formatter.setFooterText(footer);
formatter.setOutputEncoding(encoding);
formatter.setLocale(locale);
return formatter.createVisitor(output);
}
}
/**
* Formatter element for CSV reports.
*/
public class CSVFormatterElement extends FormatterElement {
private File destfile;
private String encoding = "UTF-8";
/**
* Sets the output file for the report.
*
* @param destfile
* output file
*/
public void setDestfile(final File destfile) {
this.destfile = destfile;
}
@Override
public IReportVisitor createVisitor() throws IOException {
if (destfile == null) {
throw new BuildException(
"Destination file must be supplied for csv report",
getLocation());
}
final CSVFormatter formatter = new CSVFormatter();
formatter.setOutputEncoding(encoding);
return formatter.createVisitor(new FileOutputStream(destfile));
}
/**
* Sets the output encoding for generated XML file. Default is UTF-8.
*
* @param encoding
* output encoding
*/
public void setEncoding(final String encoding) {
this.encoding = encoding;
}
}
/**
* Formatter element for XML reports.
*/
public class XMLFormatterElement extends FormatterElement {
private File destfile;
private String encoding = "UTF-8";
/**
* Sets the output file for the report.
*
* @param destfile
* output file
*/
public void setDestfile(final File destfile) {
this.destfile = destfile;
}
/**
* Sets the output encoding for generated XML file. Default is UTF-8.
*
* @param encoding
* output encoding
*/
public void setEncoding(final String encoding) {
this.encoding = encoding;
}
@Override
public IReportVisitor createVisitor() throws IOException {
if (destfile == null) {
throw new BuildException(
"Destination file must be supplied for xml report",
getLocation());
}
final XMLFormatter formatter = new XMLFormatter();
formatter.setOutputEncoding(encoding);
return formatter.createVisitor(new FileOutputStream(destfile));
}
}
/**
* Formatter element for coverage checks.
*/
public class CheckFormatterElement extends FormatterElement implements
IViolationsOutput {
private final List<Rule> rules = new ArrayList<Rule>();
private boolean violations = false;
private boolean failOnViolation = true;
private String violationsPropery = null;
/**
* Creates and adds a new rule.
*
* @return new rule
*/
public Rule createRule() {
final Rule rule = new Rule();
rules.add(rule);
return rule;
}
/**
* Sets whether the build should fail in case of a violation. Default is
* <code>true</code>.
*
* @param flag
* if <code>true</code> the build fails on violation
*/
public void setFailOnViolation(final boolean flag) {
this.failOnViolation = flag;
}
/**
* Sets the name of a property to append the violation messages to.
*
* @param property
* name of a property
*/
public void setViolationsProperty(final String property) {
this.violationsPropery = property;
}
@Override
public IReportVisitor createVisitor() throws IOException {
final RulesChecker formatter = new RulesChecker();
formatter.setRules(rules);
return formatter.createVisitor(this);
}
public void onViolation(final ICoverageNode node, final Rule rule,
final Limit limit, final String message) {
log(message, Project.MSG_ERR);
violations = true;
if (violationsPropery != null) {
final String old = getProject().getProperty(violationsPropery);
final String value = old == null ? message : String.format(
"%s\n%s", old, message);
getProject().setProperty(violationsPropery, value);
}
}
@Override
void finish() {
if (violations && failOnViolation) {
throw new BuildException(
"Coverage check failed due to violated rules.",
getLocation());
}
}
}
private final Union executiondataElement = new Union();
private SessionInfoStore sessionInfoStore;
private ExecutionDataStore executionDataStore;
private final GroupElement structure = new GroupElement();
private final List<FormatterElement> formatters = new ArrayList<FormatterElement>();
/**
* Returns the nested resource collection for execution data files.
*
* @return resource collection for execution files
*/
public Union createExecutiondata() {
return executiondataElement;
}
/**
* Returns the root group element that defines the report structure.
*
* @return root group element
*/
public GroupElement createStructure() {
return structure;
}
/**
* Creates a new HTML report element.
*
* @return HTML report element
*/
public HTMLFormatterElement createHtml() {
final HTMLFormatterElement element = new HTMLFormatterElement();
formatters.add(element);
return element;
}
/**
* Creates a new CSV report element.
*
* @return CSV report element
*/
public CSVFormatterElement createCsv() {
final CSVFormatterElement element = new CSVFormatterElement();
formatters.add(element);
return element;
}
/**
* Creates a new coverage check element.
*
* @return coverage check element
*/
public CheckFormatterElement createCheck() {
final CheckFormatterElement element = new CheckFormatterElement();
formatters.add(element);
return element;
}
/**
* Creates a new XML report element.
*
* @return CSV report element
*/
public XMLFormatterElement createXml() {
final XMLFormatterElement element = new XMLFormatterElement();
formatters.add(element);
return element;
}
@Override
public void execute() throws BuildException {
loadExecutionData();
try {
final IReportVisitor visitor = createVisitor();
visitor.visitInfo(sessionInfoStore.getInfos(),
executionDataStore.getContents());
createReport(visitor, structure);
visitor.visitEnd();
for (final FormatterElement f : formatters) {
f.finish();
}
} catch (final IOException e) {
throw new BuildException("Error while creating report", e,
getLocation());
}
}
private void loadExecutionData() {
final ExecFileLoader loader = new ExecFileLoader();
for (final Iterator<?> i = executiondataElement.iterator(); i.hasNext();) {
final Resource resource = (Resource) i.next();
log(format("Loading execution data file %s", resource));
InputStream in = null;
try {
in = resource.getInputStream();
loader.load(in);
} catch (final IOException e) {
throw new BuildException(format(
"Unable to read execution data file %s", resource), e,
getLocation());
} finally {
FileUtils.close(in);
}
}
sessionInfoStore = loader.getSessionInfoStore();
executionDataStore = loader.getExecutionDataStore();
}
private IReportVisitor createVisitor() throws IOException {
final List<IReportVisitor> visitors = new ArrayList<IReportVisitor>();
for (final FormatterElement f : formatters) {
visitors.add(f.createVisitor());
}
return new MultiReportVisitor(visitors);
}
private void createReport(final IReportGroupVisitor visitor,
final GroupElement group) throws IOException {
if (group.name == null) {
throw new BuildException("Group name must be supplied",
getLocation());
}
if (group.children.size() > 0) {
final IReportGroupVisitor groupVisitor = visitor
.visitGroup(group.name);
for (final GroupElement child : group.children) {
createReport(groupVisitor, child);
}
} else {
final IBundleCoverage bundle = createBundle(group);
final SourceFilesElement sourcefiles = group.sourcefiles;
final AntResourcesLocator locator = new AntResourcesLocator(
sourcefiles.encoding, sourcefiles.tabWidth);
locator.addAll(sourcefiles.iterator());
if (!locator.isEmpty()) {
checkForMissingDebugInformation(bundle);
}
visitor.visitBundle(bundle, locator);
}
}
private IBundleCoverage createBundle(final GroupElement group)
throws IOException {
final CoverageBuilder builder = new CoverageBuilder();
final Analyzer analyzer = new Analyzer(executionDataStore, builder);
for (final Iterator<?> i = group.classfiles.iterator(); i.hasNext();) {
final Resource resource = (Resource) i.next();
if (resource.isDirectory() && resource instanceof FileResource) {
analyzer.analyzeAll(((FileResource) resource).getFile());
} else {
final InputStream in = resource.getInputStream();
analyzer.analyzeAll(in, resource.getName());
in.close();
}
}
final IBundleCoverage bundle = builder.getBundle(group.name);
logBundleInfo(bundle, builder.getNoMatchClasses());
return bundle;
}
private void logBundleInfo(final IBundleCoverage bundle,
final Collection<IClassCoverage> nomatch) {
log(format("Writing bundle '%s' with %s classes", bundle.getName(),
Integer.valueOf(bundle.getClassCounter().getTotalCount())));
if (!nomatch.isEmpty()) {
log(format(
"Classes in bundle '%s' do no match with execution data. "
+ "For report generation the same class files must be used as at runtime.",
bundle.getName()), Project.MSG_WARN);
for (final IClassCoverage c : nomatch) {
log(format("Execution data for class %s does not match.",
c.getName()), Project.MSG_WARN);
}
}
}
private void checkForMissingDebugInformation(final ICoverageNode node) {
if (node.getClassCounter().getTotalCount() > 0
&& node.getLineCounter().getTotalCount() == 0) {
log(format(
"To enable source code annotation class files for bundle '%s' have to be compiled with debug information.",
node.getName()), Project.MSG_WARN);
}
}
}