Add JacocoCoverageRunner to junitrunner.
(series 3/4 of open-sourcing coverage command for java test)

--
PiperOrigin-RevId: 141046146
MOS_MIGRATED_REVID=141046146
diff --git a/src/java_tools/junitrunner/java/com/google/testing/coverage/JacocoCoverageRunner.java b/src/java_tools/junitrunner/java/com/google/testing/coverage/JacocoCoverageRunner.java
new file mode 100644
index 0000000..3ef86a0
--- /dev/null
+++ b/src/java_tools/junitrunner/java/com/google/testing/coverage/JacocoCoverageRunner.java
@@ -0,0 +1,278 @@
+// Copyright 2016 The Bazel Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.testing.coverage;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.Reader;
+import java.lang.reflect.Method;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+import java.util.jar.Attributes;
+import java.util.jar.JarInputStream;
+import java.util.jar.Manifest;
+import org.jacoco.agent.rt.IAgent;
+import org.jacoco.agent.rt.RT;
+import org.jacoco.core.analysis.Analyzer;
+import org.jacoco.core.analysis.CoverageBuilder;
+import org.jacoco.core.analysis.IBundleCoverage;
+import org.jacoco.core.tools.ExecFileLoader;
+import org.jacoco.report.IReportVisitor;
+import org.jacoco.report.ISourceFileLocator;
+
+/**
+ * Runner class used to generate code coverage report when using Jacoco offline instrumentation.
+ *
+ * <p>The complete list of features available for Jacoco offline instrumentation:
+ * http://www.eclemma.org/jacoco/trunk/doc/offline.html
+ *
+ * <p>The structure is roughly following the canonical Jacoco example:
+ * http://www.eclemma.org/jacoco/trunk/doc/examples/java/ReportGenerator.java
+ *
+ * <p>The following environment variables are expected:
+ * JAVA_COVERAGE_FILE - specifies final location of the generated lcov file.
+ * JACOCO_METADATA_JAR - specifies jar containing uninstrumented classes to be analyzed.
+ */
+public class JacocoCoverageRunner {
+
+  private final List<File> classesJars;
+  private final InputStream executionData;
+  private final File reportFile;
+  private ExecFileLoader execFileLoader;
+
+  public JacocoCoverageRunner(InputStream jacocoExec, String reportPath, File... metadataJars) {
+    executionData = jacocoExec;
+    reportFile = new File(reportPath);
+
+    classesJars = new ArrayList<>();
+    for (File metadataJar : metadataJars) {
+      classesJars.add(metadataJar);
+    }
+  }
+
+  public void create() throws IOException {
+    // Read the jacoco.exec file. Multiple data files could be merged at this point
+    execFileLoader = new ExecFileLoader();
+    execFileLoader.load(executionData);
+
+    // Run the structure analyzer on a single class folder or jar file to build up the coverage
+    // model. Typically you would create a bundle for each class folder and each jar you want in
+    // your report. If you have more than one bundle you may need to add a grouping node to the
+    // report. The lcov formatter doesn't seem to care, and we're only using one bundle anyway.
+    final IBundleCoverage bundleCoverage = analyzeStructure();
+
+    final Map<String, BranchCoverageDetail> branchDetails = analyzeBranch();
+    createReport(bundleCoverage, branchDetails);
+  }
+
+  private void createReport(
+      final IBundleCoverage bundleCoverage, final Map<String, BranchCoverageDetail> branchDetails)
+      throws IOException {
+    JacocoLCOVFormatter formatter = new JacocoLCOVFormatter();
+    final IReportVisitor visitor = formatter.createVisitor(reportFile, branchDetails);
+
+    // Initialize the report with all of the execution and session information. At this point the
+    // report doesn't know about the structure of the report being created.
+    visitor.visitInfo(
+        execFileLoader.getSessionInfoStore().getInfos(),
+        execFileLoader.getExecutionDataStore().getContents());
+
+    // Populate the report structure with the bundle coverage information.
+    // Call visitGroup if you need groups in your report.
+
+    // Note the API requires a sourceFileLocator because the HTML and XML formatters display a page
+    // of code annotated with coverage information. Having the source files is not actually needed
+    // for generating the lcov report...
+    visitor.visitBundle(
+        bundleCoverage,
+        new ISourceFileLocator() {
+
+          @Override
+          public Reader getSourceFile(String packageName, String fileName) throws IOException {
+            return null;
+          }
+
+          @Override
+          public int getTabWidth() {
+            return 0;
+          }
+        });
+
+    // Signal end of structure information to allow report to write all information out
+    visitor.visitEnd();
+  }
+
+  private IBundleCoverage analyzeStructure() throws IOException {
+    final CoverageBuilder coverageBuilder = new CoverageBuilder();
+    final Analyzer analyzer = new Analyzer(execFileLoader.getExecutionDataStore(), coverageBuilder);
+    for (File classesJar : classesJars) {
+      analyzer.analyzeAll(classesJar);
+    }
+
+    // TODO(bazel-team): Find out where the name of the bundle can pop out in the report.
+    return coverageBuilder.getBundle("isthisevenused");
+  }
+
+  // Additional pass to process the branch details of the classes
+  private Map<String, BranchCoverageDetail> analyzeBranch() throws IOException {
+    final BranchDetailAnalyzer analyzer =
+        new BranchDetailAnalyzer(execFileLoader.getExecutionDataStore());
+
+    Map<String, BranchCoverageDetail> result = new TreeMap<>();
+    for (File classesJar : classesJars) {
+      analyzer.analyzeAll(classesJar);
+      result.putAll(analyzer.getBranchDetails());
+    }
+    return result;
+  }
+
+  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<URL> 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 metadataJar = System.getenv("JACOCO_METADATA_JAR");
+    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]);
+                  }
+
+                  if (metadataJar != null) {
+                    // Disable coverage in this case. The build system should report an error or
+                    // warning if this happens. It's too late at this point.
+                    new JacocoCoverageRunner(dataInputStream, coverageReport, new File(metadataJar))
+                        .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 = getMainClass(metadataJar);
+    Method main =
+        Class.forName(mainClass).getMethod("main", new Class[] {java.lang.String[].class});
+    main.invoke(null, new Object[] {args});
+  }
+}