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"