Skip to content

Commit

Permalink
Update JacocoCoverageRunner and add bazel_coverage_experimental_java_…
Browse files Browse the repository at this point in the history
…test.

1) to enable coverage for executing a java binary (see 68c7e5a)
2) to collect uninstrumented class files from the system classpath in the JacocoCoverageRunner before starting the actual test.

Progress on #7124.

RELNOTES: None.
PiperOrigin-RevId: 234763346
  • Loading branch information
iirina authored and copybara-github committed Feb 20, 2019
1 parent bdaba5a commit 27ff758
Show file tree
Hide file tree
Showing 5 changed files with 520 additions and 54 deletions.
1 change: 1 addition & 0 deletions .bazelci/postsubmit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ platforms:
- "-//src/test/shell/bazel:bazel_bootstrap_distfile_test"
- "-//src/test/shell/bazel:bazel_coverage_cc_test_gcc"
- "-//src/test/shell/bazel:bazel_coverage_cc_test_llvm"
- "-//src/test/shell/bazel:bazel_coverage_experimental_java_test"
- "-//src/test/shell/bazel:bazel_coverage_java_test"
- "-//src/test/shell/bazel:bazel_coverage_sh_test"
- "-//src/test/shell/bazel:bazel_determinism_test"
Expand Down
1 change: 1 addition & 0 deletions .bazelci/presubmit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ platforms:
- "-//src/test/shell/bazel:bazel_bootstrap_distfile_test"
- "-//src/test/shell/bazel:bazel_coverage_cc_test_gcc"
- "-//src/test/shell/bazel:bazel_coverage_cc_test_llvm"
- "-//src/test/shell/bazel:bazel_coverage_experimental_java_test"
- "-//src/test/shell/bazel:bazel_coverage_java_test"
- "-//src/test/shell/bazel:bazel_coverage_sh_test"
- "-//src/test/shell/bazel:bazel_determinism_test"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableList.Builder;
import com.google.common.collect.ImmutableSet;
import com.google.common.io.ByteStreams;
import com.google.common.io.Files;
import com.google.testing.coverage.internal.BranchDetailAnalyzer;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.File;
Expand All @@ -32,7 +34,9 @@
import java.io.Reader;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
Expand Down Expand Up @@ -74,18 +78,54 @@ public class JacocoCoverageRunner {
private final File reportFile;
private final boolean isNewCoverageImplementation;
private ExecFileLoader execFileLoader;
private HashMap<String, byte[]> uninstrumentedClasses;
private ImmutableSet<String> pathsForCoverage = ImmutableSet.of();

public JacocoCoverageRunner(InputStream jacocoExec, String reportPath, File... metadataJars) {
this(false, jacocoExec, reportPath, metadataJars);
}

private JacocoCoverageRunner(boolean isNewCoverageImplementation,
InputStream jacocoExec, String reportPath, File... metadataJars) {
/**
* Creates a new coverage runner extracting the classes jars from a wrapper file. Uses
* javaRunfilesRoot to compute the absolute path of the jars inside the wrapper file.
*/
public JacocoCoverageRunner(
boolean isNewCoverageImplementation,
InputStream jacocoExec,
String reportPath,
File wrapperFile,
String javaRunfilesRoot)
throws IOException {
executionData = jacocoExec;
reportFile = new File(reportPath);
this.isNewCoverageImplementation = isNewCoverageImplementation;
this.classesJars = getFilesFromFileList(wrapperFile, javaRunfilesRoot);
}

public JacocoCoverageRunner(
boolean isNewCoverageImplementation,
InputStream jacocoExec,
String reportPath,
File... metadataJars) {
executionData = jacocoExec;
reportFile = new File(reportPath);
this.isNewCoverageImplementation = isNewCoverageImplementation;
this.classesJars = ImmutableList.copyOf(metadataJars);
}

public JacocoCoverageRunner(
boolean isNewCoverageImplementation,
InputStream jacocoExec,
String reportPath,
HashMap<String, byte[]> uninstrumentedClasses,
ImmutableSet<String> pathsForCoverage,
File... metadataJars) {
executionData = jacocoExec;
reportFile = new File(reportPath);
this.isNewCoverageImplementation = isNewCoverageImplementation;
this.classesJars = ImmutableList.copyOf(metadataJars);
this.uninstrumentedClasses = uninstrumentedClasses;
this.pathsForCoverage = pathsForCoverage;
}

public void create() throws IOException {
Expand Down Expand Up @@ -146,11 +186,17 @@ IBundleCoverage analyzeStructure() throws IOException {
final CoverageBuilder coverageBuilder = new CoverageBuilder();
final Analyzer analyzer = new Analyzer(execFileLoader.getExecutionDataStore(), coverageBuilder);
Set<String> alreadyInstrumentedClasses = new HashSet<>();
for (File classesJar : classesJars) {
if (isNewCoverageImplementation) {
analyzeUninstrumentedClassesFromJar(analyzer, classesJar, alreadyInstrumentedClasses);
} else {
analyzer.analyzeAll(classesJar);
if (uninstrumentedClasses == null) {
for (File classesJar : classesJars) {
if (isNewCoverageImplementation) {
analyzeUninstrumentedClassesFromJar(analyzer, classesJar, alreadyInstrumentedClasses);
} else {
analyzer.analyzeAll(classesJar);
}
}
} else {
for (Map.Entry<String, byte[]> entry : uninstrumentedClasses.entrySet()) {
analyzer.analyzeClass(entry.getValue(), entry.getKey());
}
}

Expand All @@ -165,11 +211,18 @@ private Map<String, BranchCoverageDetail> analyzeBranch() throws IOException {

Map<String, BranchCoverageDetail> result = new TreeMap<>();
Set<String> alreadyInstrumentedClasses = new HashSet<>();
for (File classesJar : classesJars) {
if (isNewCoverageImplementation) {
analyzeUninstrumentedClassesFromJar(analyzer, classesJar, alreadyInstrumentedClasses);
} else {
analyzer.analyzeAll(classesJar);
if (uninstrumentedClasses == null) {
for (File classesJar : classesJars) {
if (isNewCoverageImplementation) {
analyzeUninstrumentedClassesFromJar(analyzer, classesJar, alreadyInstrumentedClasses);
} else {
analyzer.analyzeAll(classesJar);
}
result.putAll(analyzer.getBranchDetails());
}
} else {
for (Map.Entry<String, byte[]> entry : uninstrumentedClasses.entrySet()) {
analyzer.analyzeClass(entry.getValue(), entry.getKey());
}
result.putAll(analyzer.getBranchDetails());
}
Expand All @@ -184,10 +237,9 @@ private Map<String, BranchCoverageDetail> analyzeBranch() throws IOException {
private void analyzeUninstrumentedClassesFromJar(
Analyzer analyzer, File jar, Set<String> 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()) {
Enumeration<JarEntry> jarFileEntries = jarFile.entries();
while (jarFileEntries.hasMoreElements()) {
JarEntry jarEntry = jarFileEntries.nextElement();
String jarEntryName = jarEntry.getName();
if (jarEntryName.endsWith(".class.uninstrumented")
&& !alreadyInstrumentedClasses.contains(jarEntryName)) {
Expand All @@ -211,11 +263,15 @@ ImmutableSet<String> createPathsSet() throws IOException {
if (!isNewCoverageImplementation) {
return ImmutableSet.<String>of();
}
if (!pathsForCoverage.isEmpty()) {
return pathsForCoverage;
}
ImmutableSet.Builder<String> execPathsSetBuilder = ImmutableSet.builder();
for (File classJar : classesJars) {
addEntriesToExecPathsSet(classJar, execPathsSetBuilder);
}
return execPathsSetBuilder.build();
ImmutableSet<String> result = execPathsSetBuilder.build();
return result;
}

/**
Expand All @@ -228,10 +284,9 @@ ImmutableSet<String> createPathsSet() throws IOException {
static void addEntriesToExecPathsSet(
File jar, ImmutableSet.Builder<String> 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()) {
Enumeration<JarEntry> jarFileEntries = jarFile.entries();
while (jarFileEntries.hasMoreElements()) {
JarEntry jarEntry = jarFileEntries.nextElement();
String jarEntryName = jarEntry.getName();
if (jarEntryName.endsWith("-paths-for-coverage.txt")) {
BufferedReader bufferedReader =
Expand All @@ -244,8 +299,13 @@ static void addEntriesToExecPathsSet(
}
}

private static String getMainClass(String metadataJar) throws Exception {
if (metadataJar != null) {
private static String getMainClass(String metadataJar, boolean isNewImplementation)
throws Exception {
final String jacocoMainClass = System.getenv("JACOCO_MAIN_CLASS");
if (jacocoMainClass != null) {
return jacocoMainClass;
}
if (!isNewImplementation && 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");
Expand All @@ -265,26 +325,12 @@ private static String getMainClass(String metadataJar) throws Exception {
}
}
throw new IllegalStateException(
"JACOCO_METADATA_JAR environment variable is not set, and no"
"JACOCO_METADATA_JAR/JACOCO_MAIN_CLASS environment variables 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.");
}
}

/**
* Returns an immutable list containing all the file paths found in mainFile. It uses the
* javaRunfilesRoot prefix for every found file to compute its absolute path.
*/
private static ImmutableList<File> getFilesFromFileList(File mainFile, String javaRunfilesRoot)
throws IOException {
List<String> metadataFiles = Files.readLines(mainFile, UTF_8);
ImmutableList.Builder<File> convertedMetadataFiles = new Builder<>();
for (String metadataFile : metadataFiles) {
convertedMetadataFiles.add(new File(javaRunfilesRoot + "/" + metadataFile));
}
return convertedMetadataFiles.build();
}

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
Expand All @@ -305,15 +351,83 @@ private static String getUniquePath(String pathTemplate, String suffix) throws I
}
}

/**
* Returns an immutable list containing all the file paths found in mainFile. It uses the
* javaRunfilesRoot prefix for every found file to compute its absolute path.
*/
private static ImmutableList<File> getFilesFromFileList(File mainFile, String javaRunfilesRoot)
throws IOException {
List<String> metadataFiles = Files.readLines(mainFile, UTF_8);
ImmutableList.Builder<File> convertedMetadataFiles = new Builder<>();
for (String metadataFile : metadataFiles) {
convertedMetadataFiles.add(new File(javaRunfilesRoot + "/" + metadataFile));
}
return convertedMetadataFiles.build();
}

public static void main(String[] args) throws Exception {
final String metadataFile = System.getenv("JACOCO_METADATA_JAR");
String metadataFile = System.getenv("JACOCO_METADATA_JAR");

String javaCoverageNewImplementationValue = System.getenv("JAVA_COVERAGE_NEW_IMPLEMENTATION");
final boolean isNewImplementation =
metadataFile == null
? false
: (metadataFile.endsWith(".txt") || metadataFile.endsWith("_merged_instr.jar"));
final boolean hasOneFile = !isNewImplementation || metadataFile.endsWith("_merged_instr.jar");
(javaCoverageNewImplementationValue != null
&& javaCoverageNewImplementationValue.equals("YES"))
|| (metadataFile == null
? false
: (metadataFile.endsWith(".txt") || metadataFile.endsWith("_merged_instr.jar")));

File[] metadataFiles = null;
final HashMap<String, byte[]> uninstrumentedClasses = new HashMap<>();
ImmutableSet.Builder<String> pathsForCoverageBuilder = new ImmutableSet.Builder<>();
if (isNewImplementation) {
ClassLoader classLoader = ClassLoader.getSystemClassLoader();
if (classLoader instanceof URLClassLoader) {
URL[] urls = ((URLClassLoader) classLoader).getURLs();
metadataFiles = new File[urls.length];
for (int i = 0; i < urls.length; i++) {
URL url = urls[i];
metadataFiles[i] = new File(url.getFile());
// Special case for deploy jars.
if (url.getFile().endsWith("_deploy.jar")) {
metadataFile = url.getFile();
} else if (url.getFile().endsWith(".jar")) {
// Collect
// - uninstrumented class files for coverage before starting the actual test
// - paths considered for coverage
// Collecting these in the shutdown hook is too expensive (we only have a 5s budget).
JarFile jarFile = new JarFile(url.getFile());
Enumeration<JarEntry> jarFileEntries = jarFile.entries();
while (jarFileEntries.hasMoreElements()) {
JarEntry jarEntry = jarFileEntries.nextElement();
String jarEntryName = jarEntry.getName();
if (jarEntryName.endsWith(".class.uninstrumented")
&& !uninstrumentedClasses.containsKey(jarEntryName)) {
uninstrumentedClasses.put(
jarEntryName, ByteStreams.toByteArray(jarFile.getInputStream(jarEntry)));
} else 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) {
pathsForCoverageBuilder.add(line);
}
}
}
}
}
}
}
final ImmutableSet<String> pathsForCoverage = pathsForCoverageBuilder.build();
final String metadataFileFinal = metadataFile;
final File[] metadataFilesFinal = metadataFiles;
final String javaRunfilesRoot = System.getenv("JACOCO_JAVA_RUNFILES_ROOT");

final boolean hasOneFile =
!isNewImplementation
|| metadataFile.endsWith("_merged_instr.jar")
|| metadataFile.endsWith("_deploy.jar");

final String coverageReportBase = System.getenv("JAVA_COVERAGE_FILE");

// Disable Jacoco's default output mechanism, which runs as a shutdown hook. We generate the
Expand Down Expand Up @@ -370,16 +484,32 @@ public void run() {
dataInputStream = new ByteArrayInputStream(new byte[0]);
}

if (metadataFile != null) {
File[] metadataJars =
hasOneFile
? new File[] {new File(metadataFile)}
: getFilesFromFileList(new File(metadataFile), javaRunfilesRoot)
.toArray(new File[0]);
if (metadataFileFinal != null || metadataFilesFinal != null) {
File[] metadataJars;
if (metadataFilesFinal != null) {
metadataJars = metadataFilesFinal;
} else {
metadataJars =
hasOneFile
? new File[] {new File(metadataFileFinal)}
: getFilesFromFileList(new File(metadataFileFinal), javaRunfilesRoot)
.toArray(new File[0]);
}

new JacocoCoverageRunner(
isNewImplementation, dataInputStream, coverageReport, metadataJars)
.create();
if (uninstrumentedClasses.isEmpty()) {
new JacocoCoverageRunner(
isNewImplementation, dataInputStream, coverageReport, metadataJars)
.create();
} else {
new JacocoCoverageRunner(
isNewImplementation,
dataInputStream,
coverageReport,
uninstrumentedClasses,
pathsForCoverage,
metadataJars)
.create();
}
}
} catch (IOException e) {
e.printStackTrace();
Expand All @@ -394,9 +524,9 @@ public void run() {
// 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);
String mainClass = getMainClass(metadataFile, isNewImplementation);
Method main = Class.forName(mainClass).getMethod("main", String[].class);
main.setAccessible(true);
main.invoke(null, new Object[] {args});
}
}
10 changes: 10 additions & 0 deletions src/test/shell/bazel/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,16 @@ sh_test(
],
)

sh_test(
name = "bazel_coverage_experimental_java_test",
srcs = ["bazel_coverage_experimental_java_test.sh"],
data = [":test-deps"],
tags = [
"local",
"no_windows",
],
)

sh_test(
name = "bazel_coverage_sh_test",
srcs = ["bazel_coverage_sh_test.sh"],
Expand Down
Loading

0 comments on commit 27ff758

Please sign in to comment.