alreadyInstrumentedClasses) throws IOException {
JarFile jarFile = new JarFile(jar);
JarInputStream jarInputStream = new JarInputStream(new FileInputStream(jar));
for (JarEntry jarEntry = jarInputStream.getNextJarEntry();
jarEntry != null;
jarEntry = jarInputStream.getNextJarEntry()) {
String jarEntryName = jarEntry.getName();
if (jarEntryName.endsWith(".class.uninstrumented")
&& !alreadyInstrumentedClasses.contains(jarEntryName)) {
analyzer.analyzeAll(jarFile.getInputStream(jarEntry), jarEntryName);
alreadyInstrumentedClasses.add(jarEntryName);
}
}
}
/**
* Creates a {@link Set} containing the paths of the covered Java files.
*
* The paths are retrieved from a txt file that is found inside each jar containing
* uninstrumented classes. Each line of the txt file represents a path to be added to the set.
*
*
This set is needed by {@link JacocoLCOVFormatter} in order to output the correct path for
* each covered class.
*/
@VisibleForTesting
ImmutableSet createPathsSet() throws IOException {
if (!isNewCoverageImplementation) {
return ImmutableSet.of();
}
ImmutableSet.Builder execPathsSetBuilder = ImmutableSet.builder();
for (File classJar : classesJars) {
addEntriesToExecPathsSet(classJar, execPathsSetBuilder);
}
return execPathsSetBuilder.build();
}
/**
* Adds to the given {@link Set} the paths found in a txt file inside the given jar.
*
* If a jar contains uninstrumented classes it will also contain a txt file with the paths of
* each of these classes, one on each line.
*/
@VisibleForTesting
static void addEntriesToExecPathsSet(
File jar, ImmutableSet.Builder execPathsSetBuilder) throws IOException {
JarFile jarFile = new JarFile(jar);
JarInputStream jarInputStream = new JarInputStream(new FileInputStream(jar));
for (JarEntry jarEntry = jarInputStream.getNextJarEntry();
jarEntry != null;
jarEntry = jarInputStream.getNextJarEntry()) {
String jarEntryName = jarEntry.getName();
if (jarEntryName.endsWith("-paths-for-coverage.txt")) {
BufferedReader bufferedReader =
new BufferedReader(new InputStreamReader(jarFile.getInputStream(jarEntry), UTF_8));
String line;
while ((line = bufferedReader.readLine()) != null) {
execPathsSetBuilder.add(line);
}
}
}
}
private static String getMainClass(String metadataJar) throws Exception {
if (metadataJar != null) {
// Blaze guarantees that JACOCO_METADATA_JAR has a proper manifest with a Main-Class entry.
try (JarInputStream jarStream = new JarInputStream(new FileInputStream(metadataJar))) {
return jarStream.getManifest().getMainAttributes().getValue("Main-Class");
}
} else {
// If metadataJar was not set, we're running inside a deploy jar. We have to open the manifest
// and read the value of "Precoverage-Class", set by Blaze. Note ClassLoader#getResource()
// will only return the first result, most likely a manifest from the bootclasspath.
Enumeration manifests =
JacocoCoverageRunner.class.getClassLoader().getResources("META-INF/MANIFEST.MF");
while (manifests.hasMoreElements()) {
Manifest manifest = new Manifest(manifests.nextElement().openStream());
Attributes attributes = manifest.getMainAttributes();
String className = attributes.getValue("Coverage-Main-Class");
if (className != null) {
return className;
}
}
throw new IllegalStateException(
"JACOCO_METADATA_JAR environment variable is not set, and no"
+ " META-INF/MANIFEST.MF on the classpath has a Coverage-Main-Class attribute. "
+ " Cannot determine the name of the main class for the code under test.");
}
}
private static String getUniquePath(String pathTemplate, String suffix) throws IOException {
// If pathTemplate is null, we're likely executing from a deploy jar and the test framework
// did not properly set the environment for coverage reporting. This alone is not a reason for
// throwing an exception, we're going to run anyway and write the coverage data to a temporary,
// throw-away file.
if (pathTemplate == null) {
return File.createTempFile("coverage", suffix).getPath();
} else {
// Blaze sets the path template to a file with the .dat extension. lcov_merger matches all
// files having '.dat' in their name, so instead of appending we change the extension.
File absolutePathTemplate = new File(pathTemplate).getAbsoluteFile();
String prefix = absolutePathTemplate.getName();
int lastDot = prefix.lastIndexOf('.');
if (lastDot != -1) {
prefix = prefix.substring(0, lastDot);
}
return File.createTempFile(prefix, suffix, absolutePathTemplate.getParentFile()).getPath();
}
}
public static void main(String[] args) throws Exception {
final String metadataFile = System.getenv("JACOCO_METADATA_JAR");
final boolean isNewImplementation =
metadataFile == null ? false : metadataFile.endsWith(".txt");
final String javaRunfilesRoot = System.getenv("JACOCO_JAVA_RUNFILES_ROOT");
final String coverageReportBase = System.getenv("JAVA_COVERAGE_FILE");
// Disable Jacoco's default output mechanism, which runs as a shutdown hook. We generate the
// report in our own shutdown hook below, and we want to avoid the data race (shutdown hooks are
// not guaranteed any particular order). Note that also by default, Jacoco appends coverage
// data, which can have surprising results if running tests locally or somehow encountering
// the previous .exec file.
System.setProperty("jacoco-agent.output", "none");
// We have no use for this sessionId property, but leaving it blank results in a DNS lookup
// at runtime. A minor annoyance: the documentation insists the property name is "sessionId",
// however on closer inspection of the source code, it turns out to be "sessionid"...
System.setProperty("jacoco-agent.sessionid", "default");
// A JVM shutdown hook has a fixed amount of time (OS-dependent) before it is terminated.
// For our purpose, it's more than enough to scan through the instrumented jar and match up
// the bytecode with the coverage data. It wouldn't be enough for scanning the entire classpath,
// or doing something else terribly inefficient.
Runtime.getRuntime()
.addShutdownHook(
new Thread() {
@Override
public void run() {
try {
// If the test spawns multiple JVMs, they will race to write to the same files. We
// need to generate unique paths for each execution. lcov_merger simply collects
// all the .dat files in the current directory anyway, so we don't need to worry
// about merging them.
String coverageReport = getUniquePath(coverageReportBase, ".dat");
String coverageData = getUniquePath(coverageReportBase, ".exec");
// Get a handle on the Jacoco Agent and write out the coverage data. Other options
// included talking to the agent via TCP (useful when gathering coverage from
// multiple JVMs), or via JMX (the agent's MXBean is called
// 'org.jacoco:type=Runtime'). As we're running in the same JVM, these options
// seemed overkill, we can just refer to the Jacoco runtime as RT.
// See http://www.eclemma.org/jacoco/trunk/doc/agent.html for all the options
// available.
ByteArrayInputStream dataInputStream;
try {
IAgent agent = RT.getAgent();
byte[] data = agent.getExecutionData(false);
try (FileOutputStream fs = new FileOutputStream(coverageData, true)) {
fs.write(data);
}
// We append to the output file, but run report generation only for the coverage
// data from this JVM. The output file may contain data from other
// subprocesses, etc.
dataInputStream = new ByteArrayInputStream(data);
} catch (IllegalStateException e) {
// In this case, we didn't execute a single instrumented file, so the agent
// isn't live. There's no coverage to report, but it's otherwise a successful
// invocation.
dataInputStream = new ByteArrayInputStream(new byte[0]);
}
File[] metadataJars;
if (metadataFile != null) {
if (isNewImplementation) {
List metadataFiles = Files.readLines(new File(metadataFile), UTF_8);
List convertedMetadataFiles = new ArrayList<>();
for (String metadataFile : metadataFiles) {
convertedMetadataFiles.add(new File(javaRunfilesRoot + metadataFile));
}
metadataJars = convertedMetadataFiles.toArray(new File[0]);
} else {
metadataJars = new File[] {new File(metadataFile)};
}
new JacocoCoverageRunner(
isNewImplementation, dataInputStream, coverageReport, metadataJars)
.create();
}
} catch (IOException e) {
e.printStackTrace();
Runtime.getRuntime().halt(1);
}
}
});
// Another option would be to run the tests in a separate JVM, let Jacoco dump out the coverage
// data, wait for the subprocess to finish and then generate the lcov report. The only benefit
// of doing this is not being constrained by the hard 5s limit of the shutdown hook. Setting up
// the subprocess to match all JVM flags, runtime classpath, bootclasspath, etc is doable.
// We'd share the same limitation if the system under test uses shutdown hooks internally, as
// there's no way to collect coverage data on that code.
String mainClass =
isNewImplementation ? System.getenv("JACOCO_MAIN_CLASS") : getMainClass(metadataFile);
Method main = Class.forName(mainClass).getMethod("main", String[].class);
main.invoke(null, new Object[] {args});
}
}