Skip to content

Instantly share code, notes, and snippets.

@wvanderdeijl
Last active September 23, 2015 11:13
Show Gist options
  • Save wvanderdeijl/033069182e92d2df1b6c to your computer and use it in GitHub Desktop.
Save wvanderdeijl/033069182e92d2df1b6c to your computer and use it in GitHub Desktop.
JacocoReporter to write a HTML report from a Jacoco execution data file
package org.adfemg.datacontrol.view.uitest;
import java.io.IOException;
import java.io.Reader;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.logging.Logger;
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.data.ExecutionDataStore;
import org.jacoco.core.tools.ExecFileLoader;
import org.jacoco.report.FileMultiReportOutput;
import org.jacoco.report.IMultiReportOutput;
import org.jacoco.report.IReportVisitor;
import org.jacoco.report.ISourceFileLocator;
import org.jacoco.report.html.HTMLFormatter;
/**
* A Reporter that can take a Jacoco execution data file and transform it into a human readable html report.
* As this reporter has quite a few configuration properties, it includes a Builder to conveniently configure your
* reporter. See the documentation on these builder methods to see the exact behavior of this rule.
* Such a JacocoReporter can be invoked using the {@link #report(ExecFileLoader, String, String) method but it is typically
* used as the argument to {@link JacocoRule.Builder#withReporter(JacocoReporter)} to automatically create the html
* reporter when the junit test completes.
* @see #builder
*/
public class JacocoReporter {
private final List<Path> classes;
private final Path outputbasedir;
private final List<Path> src;
private final Charset srcCharset;
private final int srcTabWidth;
private String name;
private String title;
private static final Logger log = Logger.getLogger(JacocoReporter.class.getName());
/**
* Private constructor. See {@link #builder()} and {@link Builder} for a builder to create and configure a
* JacocoReporter.
* @param classes List of directories to (recursively) scan for java .class files to report the coverage on.
* @param outputdir Base directory used to write the report to. The actual report will be written to a subdirectory
* of this base directory determined by the arguments to {@link #report}
* @param src List of directories to (recusively) scan for .java source files of the .class files to report the
* coverage on
* @param srcCharset character set used to read the source files
* @param srcTabWidth how many space characters should be used when interpreting a tab character in the sources
*/
private JacocoReporter(final List<Path> classes, final Path outputbasedir, final List<Path> src,
final Charset srcCharset, final int srcTabWidth) {
this.classes = Collections.unmodifiableList(new ArrayList<Path>(classes));
this.outputbasedir = outputbasedir;
this.src = Collections.unmodifiableList(new ArrayList<Path>(src));
this.srcCharset = srcCharset;
this.srcTabWidth = srcTabWidth;
}
/**
* Static method to get a {@link Builder} to configure and create a new JacocoReporter.
* @return Builder
*/
public static Builder builder() {
return new Builder();
}
/**
* Create a human readable html report from a given jacoco execution dump file.
* @param dump jacoco execution data file
* @param name the name of the report (directory) to create within the report base directory to write the
* report in. This typically refers to the name of the test being run.
* @param title title to use at the top of the report pages to be generated
* @throws IOException
*/
public void report(final ExecFileLoader dump, final String name, final String title) throws IOException {
this.name = name;
this.title = title;
log.info("creating code coverage html report in " + getReportDir());
final IReportVisitor visitor = createVisitor();
visitor.visitInfo(dump.getSessionInfoStore().getInfos(), dump.getExecutionDataStore().getContents());
final ISourceFileLocator sourceslocator = createSourceLocator();
final IBundleCoverage bundle = createBundle(dump.getExecutionDataStore());
visitor.visitBundle(bundle, sourceslocator);
visitor.visitEnd();
}
/**
* Create a visitor that writes a report to a certain location.
* @return an IReportVisitor that can write a report to the directory given to {@link Builder#withOutputDir(Path)}
* @throws IOException
*/
private IReportVisitor createVisitor() throws IOException {
final IMultiReportOutput output;
output = new FileMultiReportOutput(getReportDir().toFile());
final HTMLFormatter formatter = new HTMLFormatter();
return formatter.createVisitor(output);
}
/**
* Create a locator that can look-up source files that will be included in the report.
* @return an ISourceFileLocator that reads in all the source directories that were supplied to
* {@link Builder#withSrc(Path)}
*/
private ISourceFileLocator createSourceLocator() {
return new ISourceFileLocator() {
@Override
public Reader getSourceFile(String packageName, String fileName) throws IOException {
for (Path srcdir : src) {
Path f = srcdir.resolve(packageName).resolve(fileName);
if (Files.exists(f) && !Files.isDirectory(f) && Files.isReadable(f)) {
return Files.newBufferedReader(f, srcCharset);
}
}
return null; // not found
}
@Override
public int getTabWidth() {
return srcTabWidth;
}
};
}
/**
* Given the execution data, build a IBundleCoverage that contains the coverage data for a collection of java
* packages.
* @param executionDataStore code coverage execution data
* @return an IBundleCoverage that reports on coverage for all classes from the directories given to
* {@link Builder#withClasses(Path)}
* @throws IOException
*/
private IBundleCoverage createBundle(final ExecutionDataStore executionDataStore) throws IOException {
final CoverageBuilder builder = new CoverageBuilder();
final Analyzer analyzer = new Analyzer(executionDataStore, builder);
for (Path p : classes) {
analyzer.analyzeAll(p.toFile());
}
final IBundleCoverage bundle = builder.getBundle(title);
logBundleInfo(bundle, builder.getNoMatchClasses());
return bundle;
}
private void logBundleInfo(final IBundleCoverage bundle, final Collection<IClassCoverage> nomatch) {
log.info("Writing bundle '" + bundle.getName() + "' with " + bundle.getClassCounter().getTotalCount() +
" classes");
if (!nomatch.isEmpty()) {
log.warning("Classes in report '" + bundle.getName() + "' do not match with execution data. " +
"For report generation the same class files must be used as at runtime.");
for (final IClassCoverage c : nomatch) {
log.warning("Execution data for class " + c.getName() + " does not match.");
}
}
}
private Path getReportDir() throws IOException {
return outputbasedir.resolve(name).normalize().toAbsolutePath();
}
/**
* Builder to configure and create a new JacocoReporter. Each configuration method returns this same builder after
* changing its internal state. This can be used to chain multiple method invocations in a single statement. The
* final method being called should be {@link #build} which actually returns the configured JacocoReporter.
* <p>
* See each method for an explanation of the separate configurable items.
* <p>
* Example
* {@code JacocoReporter.builder().withClasses(Paths.get("classes")).withOutputDir(Paths.get("report")).build();}
*/
public static class Builder {
private List<Path> classes = DFLT_CLASSES_PATH;
private Path outputbasedir = DFLT_OUTPUT_PATH;
private List<Path> src = DFLT_SRC_PATH;
private Charset srcCharset = Charset.defaultCharset();
private int srcTabWidth = DFLT_TAB_WIDTH;
private static final List<Path> DFLT_CLASSES_PATH = Collections.singletonList(Paths.get("classes"));
private static final Path DFLT_OUTPUT_PATH = Paths.get("");
private static final List<Path> DFLT_SRC_PATH = Collections.singletonList(Paths.get("src"));
private static final int DFLT_TAB_WIDTH = 4;
/**
* Private constructor.
* @see JacocoReporter#builder
*/
private Builder() {
}
/**
* Add a directory to the list of directories to (recursively) search for .class files. When this configuration
* method is never invoked the default would be to look in a directory "classes" from the current working
* directory. This default is erased on the first invocation of this method. This method can be invoked
* multiple times to add more paths to the search path.
* @param dir directory to add to the list of paths to search. Can be an absolute path or a relative path that
* wilol be evaluated from the current working directory.
* @return Builder so you can continue invoking configuration methods
*/
public Builder withClasses(final Path dir) {
if (classes == DFLT_CLASSES_PATH) {
classes = new ArrayList<Path>();
}
classes.add(dir);
return this;
}
/**
* Do not write to the current working directory but to the given output directory. Please note that
* this is only the base directory for reporting. The actual report will be in a subdirectory of this base
* directory where the subdirectory name can differ for each time this reporter is invoked.
* @param dir base directory for reports
* @return Builder so you can continue invoking configuration methods
*/
public Builder withOutputBaseDir(final Path dir) {
this.outputbasedir = dir;
return this;
}
/**
* Add a directory to the list of directories to (recursively) search for .java source files. When this
* configuration method is never invoked the default would be to look in a directory "src" from the current
* working directory. This default is erased on the first invocation of this method. This method can be invoked
* multiple times to add more paths to the search path.
* @param dir directory to add to the list of paths to search. Can be an absolute path or a relative path that
* wilol be evaluated from the current working directory.
* @return Builder so you can continue invoking configuration methods
* @see #withSrcCharset
*/
public Builder withSrc(final Path dir) {
if (src == DFLT_SRC_PATH) {
src = new ArrayList<Path>();
}
src.add(dir);
return this;
}
/**
* Do not use the default character set to read the source files but use the given character set to read them.
* @param charset character set to use for reading the source files
* @return Builder so you can continue invoking configuration methods
* @see withSrcCharset(String)
*/
public Builder withSrcCharset(final Charset charset) {
this.srcCharset = charset;
return this;
}
/**
* Do not use the default character set to read the source files but use the given character set to read them.
* @param charset character set to use for reading the source files
* @return Builder so you can continue invoking configuration methods
* @see withSrcCharset(Charset)
*/
public Builder withSrcCharset(final String charset) {
this.srcCharset = Charset.forName(charset);
return this;
}
/**
* Do not use the default width (@{value DFLT_TAB_WIDTH}) to interprete tab stops in the source files, but
* use the given width.
* @param spaces number of space characters to use when interpreting a tab character
* @return Builder so you can continue invoking configuration methods
*/
public Builder withSrcTabWidth(final int spaces) {
this.srcTabWidth = spaces;
return this;
}
/**
* Construct a JacocoReporter with all the configuration given to this builder.
* @return a fully (and immutable) JacocoReporter instance.
*/
public JacocoReporter build() {
return new JacocoReporter(classes, outputbasedir, src, srcCharset, srcTabWidth);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment