Last active
September 23, 2015 11:13
-
-
Save wvanderdeijl/033069182e92d2df1b6c to your computer and use it in GitHub Desktop.
JacocoReporter to write a HTML report from a Jacoco execution data file
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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