Update JacocoCoverageRunner and add bazel_coverage_experimental_java_test.

1) to enable coverage for executing a java binary (see https://github.com/bazelbuild/bazel/commit/68c7e5a3c679be51f750d44aae146007f0f04b4d)
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
diff --git a/.bazelci/postsubmit.yml b/.bazelci/postsubmit.yml
index 5a7683d..84bb31f 100644
--- a/.bazelci/postsubmit.yml
+++ b/.bazelci/postsubmit.yml
@@ -82,6 +82,7 @@
     - "-//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"
diff --git a/.bazelci/presubmit.yml b/.bazelci/presubmit.yml
index 2dcf73a..42fa917 100644
--- a/.bazelci/presubmit.yml
+++ b/.bazelci/presubmit.yml
@@ -104,6 +104,7 @@
     - "-//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"
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
index 2a4d3db..3d16bd4 100644
--- a/src/java_tools/junitrunner/java/com/google/testing/coverage/JacocoCoverageRunner.java
+++ b/src/java_tools/junitrunner/java/com/google/testing/coverage/JacocoCoverageRunner.java
@@ -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;
@@ -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;
@@ -74,18 +78,54 @@
   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.classesJars = ImmutableList.copyOf(metadataJars);
     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 {
@@ -146,11 +186,17 @@
     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());
       }
     }
 
@@ -165,11 +211,18 @@
 
     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());
     }
@@ -184,10 +237,9 @@
   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)) {
@@ -211,11 +263,15 @@
     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;
   }
 
   /**
@@ -228,10 +284,9 @@
   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 =
@@ -244,8 +299,13 @@
     }
   }
 
-  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");
@@ -265,26 +325,12 @@
         }
       }
       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
@@ -305,15 +351,83 @@
     }
   }
 
+  /**
+   * 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
@@ -370,16 +484,32 @@
                     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();
@@ -394,9 +524,9 @@
     // 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});
   }
 }
diff --git a/src/test/shell/bazel/BUILD b/src/test/shell/bazel/BUILD
index dc9f303..ec18516 100644
--- a/src/test/shell/bazel/BUILD
+++ b/src/test/shell/bazel/BUILD
@@ -232,6 +232,16 @@
 )
 
 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"],
     data = [":test-deps"],
diff --git a/src/test/shell/bazel/bazel_coverage_experimental_java_test.sh b/src/test/shell/bazel/bazel_coverage_experimental_java_test.sh
new file mode 100755
index 0000000..fbfb77b
--- /dev/null
+++ b/src/test/shell/bazel/bazel_coverage_experimental_java_test.sh
@@ -0,0 +1,324 @@
+#!/bin/bash
+#
+# Copyright 2015 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.
+
+set -eu
+
+# Load the test setup defined in the parent directory
+CURRENT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+source "${CURRENT_DIR}/../integration_test_setup.sh" \
+  || { echo "integration_test_setup.sh not found!" >&2; exit 1; }
+
+add_to_bazelrc "coverage --experimental_java_coverage"
+
+# Asserts if the given expected coverage result is included in the given output
+# file.
+#
+# - expected_coverage The expected result that must be included in the output.
+# - output_file       The location of the coverage output file.
+function assert_coverage_result() {
+    local expected_coverage="${1}"; shift
+    local output_file="${1}"; shift
+
+    # Replace newlines with commas to facilitate the assertion.
+    local expected_coverage_no_newlines="$( echo "$expected_coverage" | tr '\n' ',' )"
+    local output_file_no_newlines="$( cat "$output_file" | tr '\n' ',' )"
+
+    (echo "$output_file_no_newlines" \
+        | grep -F "$expected_coverage_no_newlines") \
+        || fail "Expected coverage result
+<$expected_coverage>
+was not found in actual coverage report:
+<$( cat "$output_file" )>"
+}
+
+# Returns the path of the code coverage report that was generated by Bazel by
+# looking at the current $TEST_log. The method fails if TEST_log does not
+# contain any coverage report for a passed test.
+function get_coverage_file_path_from_test_log() {
+  local ending_part="$(sed -n -e '/PASSED/,$p' "$TEST_log")"
+
+  local coverage_file_path=$(grep -Eo "/[/a-zA-Z0-9\.\_\-]+\.dat$" <<< "$ending_part")
+  [[ -e "$coverage_file_path" ]] || fail "Coverage output file does not exist!"
+  echo "$coverage_file_path"
+}
+
+function test_java_test_coverage() {
+  cat <<EOF > BUILD
+java_test(
+    name = "test",
+    srcs = glob(["src/test/**/*.java"]),
+    test_class = "com.example.TestCollatz",
+    deps = [":collatz-lib"],
+)
+
+java_library(
+    name = "collatz-lib",
+    srcs = glob(["src/main/**/*.java"]),
+)
+EOF
+
+  mkdir -p src/main/com/example
+  cat <<EOF > src/main/com/example/Collatz.java
+package com.example;
+
+public class Collatz {
+
+  public static int getCollatzFinal(int n) {
+    if (n == 1) {
+      return 1;
+    }
+    if (n % 2 == 0) {
+      return getCollatzFinal(n / 2);
+    } else {
+      return getCollatzFinal(n * 3 + 1);
+    }
+  }
+
+}
+EOF
+
+  mkdir -p src/test/com/example
+  cat <<EOF > src/test/com/example/TestCollatz.java
+package com.example;
+
+import static org.junit.Assert.assertEquals;
+import org.junit.Test;
+
+public class TestCollatz {
+
+  @Test
+  public void testGetCollatzFinal() {
+    assertEquals(Collatz.getCollatzFinal(1), 1);
+    assertEquals(Collatz.getCollatzFinal(5), 1);
+    assertEquals(Collatz.getCollatzFinal(10), 1);
+    assertEquals(Collatz.getCollatzFinal(21), 1);
+  }
+
+}
+EOF
+
+  bazel coverage --test_output=all //:test &>$TEST_log || fail "Coverage for //:test failed"
+  cat $TEST_log
+  local coverage_file_path="$( get_coverage_file_path_from_test_log )"
+
+  cat <<EOF > result.dat
+SF:src/main/com/example/Collatz.java
+FN:3,com/example/Collatz::<init> ()V
+FN:6,com/example/Collatz::getCollatzFinal (I)I
+FNDA:0,com/example/Collatz::<init> ()V
+FNDA:1,com/example/Collatz::getCollatzFinal (I)I
+FNF:2
+FNH:1
+BA:6,2
+BA:9,2
+BRF:2
+BRH:2
+DA:3,0
+DA:6,3
+DA:7,2
+DA:9,4
+DA:10,5
+DA:12,7
+LH:5
+LF:6
+end_of_record
+EOF
+
+  diff result.dat "$coverage_file_path" >> $TEST_log
+  if ! cmp result.dat $coverage_file_path; then
+    fail "Coverage output file is different with expected"
+  fi
+}
+
+function test_java_test_coverage_combined_report() {
+
+  cat <<EOF > BUILD
+java_test(
+    name = "test",
+    srcs = glob(["src/test/**/*.java"]),
+    test_class = "com.example.TestCollatz",
+    deps = [":collatz-lib"],
+)
+
+java_library(
+    name = "collatz-lib",
+    srcs = glob(["src/main/**/*.java"]),
+)
+EOF
+
+  mkdir -p src/main/com/example
+  cat <<EOF > src/main/com/example/Collatz.java
+package com.example;
+
+public class Collatz {
+
+  public static int getCollatzFinal(int n) {
+    if (n == 1) {
+      return 1;
+    }
+    if (n % 2 == 0) {
+      return getCollatzFinal(n / 2);
+    } else {
+      return getCollatzFinal(n * 3 + 1);
+    }
+  }
+
+}
+EOF
+
+  mkdir -p src/test/com/example
+  cat <<EOF > src/test/com/example/TestCollatz.java
+package com.example;
+
+import static org.junit.Assert.assertEquals;
+import org.junit.Test;
+
+public class TestCollatz {
+
+  @Test
+  public void testGetCollatzFinal() {
+    assertEquals(Collatz.getCollatzFinal(1), 1);
+    assertEquals(Collatz.getCollatzFinal(5), 1);
+    assertEquals(Collatz.getCollatzFinal(10), 1);
+    assertEquals(Collatz.getCollatzFinal(21), 1);
+  }
+
+}
+EOF
+
+  bazel coverage --test_output=all //:test --coverage_report_generator=@bazel_tools//tools/test/CoverageOutputGenerator/java/com/google/devtools/coverageoutputgenerator:Main --combined_report=lcov &>$TEST_log \
+   || echo "Coverage for //:test failed"
+
+  cat <<EOF > result.dat
+SF:src/main/com/example/Collatz.java
+FN:3,com/example/Collatz::<init> ()V
+FN:6,com/example/Collatz::getCollatzFinal (I)I
+FNDA:0,com/example/Collatz::<init> ()V
+FNDA:1,com/example/Collatz::getCollatzFinal (I)I
+FNF:2
+FNH:1
+BA:6,2
+BA:9,2
+BRF:2
+BRH:2
+DA:3,0
+DA:6,3
+DA:7,2
+DA:9,4
+DA:10,5
+DA:12,7
+LH:5
+LF:6
+end_of_record
+EOF
+
+  if ! cmp result.dat ./bazel-out/_coverage/_coverage_report.dat; then
+    diff result.dat bazel-out/_coverage/_coverage_report.dat >> $TEST_log
+    fail "Coverage output file is different with expected"
+  fi
+}
+
+function test_java_test_java_import_coverage() {
+
+  cat <<EOF > BUILD
+java_test(
+    name = "test",
+    srcs = glob(["src/test/**/*.java"]),
+    test_class = "com.example.TestCollatz",
+    deps = [":collatz-import"],
+)
+
+java_import(
+    name = "collatz-import",
+    jars = [":libcollatz-lib.jar"],
+)
+
+java_library(
+    name = "collatz-lib",
+    srcs = glob(["src/main/**/*.java"]),
+)
+EOF
+
+  mkdir -p src/main/com/example
+  cat <<EOF > src/main/com/example/Collatz.java
+package com.example;
+
+public class Collatz {
+
+  public static int getCollatzFinal(int n) {
+    if (n == 1) {
+      return 1;
+    }
+    if (n % 2 == 0) {
+      return getCollatzFinal(n / 2);
+    } else {
+      return getCollatzFinal(n * 3 + 1);
+    }
+  }
+
+}
+EOF
+
+  mkdir -p src/test/com/example
+  cat <<EOF > src/test/com/example/TestCollatz.java
+package com.example;
+
+import static org.junit.Assert.assertEquals;
+import org.junit.Test;
+
+public class TestCollatz {
+
+  @Test
+  public void testGetCollatzFinal() {
+    assertEquals(Collatz.getCollatzFinal(1), 1);
+    assertEquals(Collatz.getCollatzFinal(5), 1);
+    assertEquals(Collatz.getCollatzFinal(10), 1);
+    assertEquals(Collatz.getCollatzFinal(21), 1);
+  }
+
+}
+EOF
+
+  bazel coverage --test_output=all //:test &>$TEST_log || fail "Coverage for //:test failed"
+  local coverage_file_path="$( get_coverage_file_path_from_test_log )"
+
+  cat <<EOF > result.dat
+SF:src/main/com/example/Collatz.java
+FN:3,com/example/Collatz::<init> ()V
+FN:6,com/example/Collatz::getCollatzFinal (I)I
+FNDA:0,com/example/Collatz::<init> ()V
+FNDA:1,com/example/Collatz::getCollatzFinal (I)I
+FNF:2
+FNH:1
+BA:6,2
+BA:9,2
+BRF:2
+BRH:2
+DA:3,0
+DA:6,3
+DA:7,2
+DA:9,4
+DA:10,5
+DA:12,7
+LH:5
+LF:6
+end_of_record
+EOF
+  diff result.dat "$coverage_file_path" >> $TEST_log
+  cmp result.dat "$coverage_file_path" || fail "Coverage output file is different than the expected file"
+}
+
+run_suite "test tests"