aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorGravatar Yue Gan <yueg@google.com>2016-12-05 14:36:02 +0000
committerGravatar Damien Martin-Guillerez <dmarting@google.com>2016-12-05 15:03:03 +0000
commitaf3c41225ed65f801bb2078a12743494411a375a (patch)
tree5b16d36c5e57a22ddaeef4fba3f79a323c8305a5
parent21759e7695e302f18fc60cb9f17a8e68199ceeea (diff)
Add JacocoCoverageRunner to junitrunner.
(series 3/4 of open-sourcing coverage command for java test) -- PiperOrigin-RevId: 141046146 MOS_MIGRATED_REVID=141046146
-rw-r--r--src/BUILD3
-rw-r--r--src/java_tools/junitrunner/java/com/google/testing/coverage/BUILD71
-rw-r--r--src/java_tools/junitrunner/java/com/google/testing/coverage/BUILD.tools8
-rw-r--r--src/java_tools/junitrunner/java/com/google/testing/coverage/BitField.java209
-rw-r--r--src/java_tools/junitrunner/java/com/google/testing/coverage/BranchCoverageDetail.java86
-rw-r--r--src/java_tools/junitrunner/java/com/google/testing/coverage/BranchDetailAnalyzer.java86
-rw-r--r--src/java_tools/junitrunner/java/com/google/testing/coverage/BranchExp.java81
-rw-r--r--src/java_tools/junitrunner/java/com/google/testing/coverage/ClassProbesMapper.java59
-rw-r--r--src/java_tools/junitrunner/java/com/google/testing/coverage/CovExp.java24
-rw-r--r--src/java_tools/junitrunner/java/com/google/testing/coverage/IllegalStringException.java26
-rw-r--r--src/java_tools/junitrunner/java/com/google/testing/coverage/JacocoCoverageRunner.java278
-rw-r--r--src/java_tools/junitrunner/java/com/google/testing/coverage/JacocoLCOVFormatter.java151
-rw-r--r--src/java_tools/junitrunner/java/com/google/testing/coverage/MethodProbesMapper.java403
-rw-r--r--src/java_tools/junitrunner/java/com/google/testing/coverage/ProbeExp.java29
14 files changed, 1514 insertions, 0 deletions
diff --git a/src/BUILD b/src/BUILD
index ee9ae68800..b70121bec8 100644
--- a/src/BUILD
+++ b/src/BUILD
@@ -148,9 +148,11 @@ genrule(
"//src/java_tools/singlejar:SingleJar_deploy.jar",
"//src/java_tools/buildjar/java/com/google/devtools/build/buildjar/genclass:GenClass_deploy.jar",
"//src/java_tools/junitrunner/java/com/google/testing/junit/runner:Runner_deploy.jar",
+ "//src/java_tools/junitrunner/java/com/google/testing/coverage:embedded_tools",
"//third_party/ijar",
"//third_party/java/apkbuilder:embedded_tools",
"//third_party/java/apksig:embedded_tools",
+ "//third_party/java/jacoco:srcs",
] + select({
":darwin": [
":darwin_tools",
@@ -284,6 +286,7 @@ filegroup(
name = "srcs",
srcs = glob(["**"]) + [
"//src/java_tools/buildjar:srcs",
+ "//src/java_tools/junitrunner/java/com/google/testing/coverage:srcs",
"//src/java_tools/junitrunner/java/com/google/testing/junit/junit4:srcs",
"//src/java_tools/junitrunner/java/com/google/testing/junit/runner:srcs",
"//src/java_tools/junitrunner/java/com/google/testing/junit/runner/internal:srcs",
diff --git a/src/java_tools/junitrunner/java/com/google/testing/coverage/BUILD b/src/java_tools/junitrunner/java/com/google/testing/coverage/BUILD
new file mode 100644
index 0000000000..863bcc060c
--- /dev/null
+++ b/src/java_tools/junitrunner/java/com/google/testing/coverage/BUILD
@@ -0,0 +1,71 @@
+package(
+ default_visibility = ["//visibility:legacy_public"],
+)
+
+licenses(["notice"])
+
+filegroup(
+ name = "bazel-srcs",
+ testonly = 0,
+ srcs = glob([
+ "**/*.java",
+ "BUILD.tools",
+ ]),
+ visibility = ["//third_party/bazel:__subpackages__"],
+)
+
+filegroup(
+ name = "srcs",
+ srcs = glob(["**"]),
+)
+
+filegroup(
+ name = "embedded_tools",
+ srcs = [
+ "BUILD.tools",
+ "JacocoCoverage_deploy.jar",
+ ],
+)
+
+filegroup(
+ name = "jacoco_coverage_runtime",
+ srcs = ["JacocoCoverage_deploy.jar"],
+)
+
+# Bazel custom Jacoco runner used to provide proper initialization and lcov
+# report generation when using offline Jacoco instrumentation.
+# This target should not be used as a dependency (except when writing tests for
+# it).
+#
+# An implicit dependency of all "java_binary" rules.
+java_binary(
+ name = "JacocoCoverage",
+ srcs = [
+ "BranchCoverageDetail.java",
+ "BranchDetailAnalyzer.java",
+ "BranchExp.java",
+ "ClassProbesMapper.java",
+ "CovExp.java",
+ "JacocoCoverageRunner.java",
+ "JacocoLCOVFormatter.java",
+ "MethodProbesMapper.java",
+ "ProbeExp.java",
+ ],
+ deps = [
+ ":bitfield",
+ "//third_party/java/jacoco:blaze-agent-neverlink",
+ "//third_party/java/jacoco:core",
+ "//third_party/java/jacoco:report",
+ ],
+)
+
+java_library(
+ name = "bitfield",
+ srcs = [
+ "BitField.java",
+ "IllegalStringException.java",
+ ],
+ deps = [
+ "//third_party:apache_commons_lang",
+ ],
+)
diff --git a/src/java_tools/junitrunner/java/com/google/testing/coverage/BUILD.tools b/src/java_tools/junitrunner/java/com/google/testing/coverage/BUILD.tools
new file mode 100644
index 0000000000..c52cacce2a
--- /dev/null
+++ b/src/java_tools/junitrunner/java/com/google/testing/coverage/BUILD.tools
@@ -0,0 +1,8 @@
+package(
+ default_visibility = ["//visibility:public"],
+)
+
+java_import(
+ name = "JacocoCoverage",
+ jars = ["JacocoCoverage_deploy.jar"],
+)
diff --git a/src/java_tools/junitrunner/java/com/google/testing/coverage/BitField.java b/src/java_tools/junitrunner/java/com/google/testing/coverage/BitField.java
new file mode 100644
index 0000000000..5967677578
--- /dev/null
+++ b/src/java_tools/junitrunner/java/com/google/testing/coverage/BitField.java
@@ -0,0 +1,209 @@
+// 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.util.Arrays;
+import org.apache.commons.lang.builder.EqualsBuilder;
+import org.apache.commons.lang.builder.HashCodeBuilder;
+
+/**
+ * Abstracts bit field operations.
+ */
+public class BitField {
+
+ private byte[] bytes;
+
+ private static final int[] BIT_COUNT_LOOKUP = new int[256];
+
+ static {
+ BIT_COUNT_LOOKUP[0] = 0;
+ BIT_COUNT_LOOKUP[1] = 1;
+ for (int i = 2; i < 256; i += 2) {
+ int count = BIT_COUNT_LOOKUP[i / 2];
+ BIT_COUNT_LOOKUP[i] = count;
+ BIT_COUNT_LOOKUP[i + 1] = count + 1;
+ }
+ }
+
+ public BitField() {
+ this(new byte[0]);
+ }
+
+ public BitField(byte[] bytes) {
+ this.bytes = bytes.clone();
+ }
+
+ /**
+ * Returns a copy of the underlying byte array.
+ *
+ * @return byte array copy
+ */
+ public byte[] getBytes() {
+ return bytes.clone();
+ }
+
+ /**
+ * Sets or clears a bit at the given index.
+ *
+ * @param index bit index
+ */
+ public void setBit(int index) {
+ setBit(index, true);
+ }
+
+ /**
+ * Clears a bit at the given index
+ *
+ * @param index bit index
+ */
+ public void clearBit(int index) {
+ setBit(index, false);
+ }
+
+ /**
+ * Sets or clears a bit at the given index.
+ *
+ * @param index bit index
+ */
+ private void setBit(int index, boolean isSet) {
+ int byteIndex = index / 8;
+ int newByteSize = byteIndex + 1;
+ if (bytes.length < newByteSize) {
+ bytes = Arrays.copyOf(bytes, newByteSize);
+ }
+
+ int bitIndex = index % 8;
+ int mask = 1 << bitIndex;
+
+ if (isSet) {
+ bytes[byteIndex] = (byte) (bytes[byteIndex] | mask);
+ } else {
+ bytes[byteIndex] = (byte) (bytes[byteIndex] & ~mask);
+ }
+ }
+
+ /**
+ * Checks whether a bit at the given index is set.
+ *
+ * @param index bit index
+ * @return true if set, false otherwise
+ */
+ public boolean isBitSet(int index) {
+ int byteIndex = index / 8;
+
+ if (byteIndex >= bytes.length) {
+ return false;
+ }
+
+ int bitIndex = index % 8;
+ int mask = 1 << bitIndex;
+ return (bytes[byteIndex] & mask) != 0;
+ }
+
+ /** Performs a non-destructive bit-wise "and" of this bit field with another one. */
+ public BitField and(BitField other) {
+ int size = Math.min(bytes.length, other.bytes.length);
+ byte[] result = new byte[size];
+
+ for (int i = 0; i < size; i++) {
+ result[i] = (byte) (bytes[i] & other.bytes[i]);
+ }
+
+ return new BitField(result);
+ }
+
+ /**
+ * Performs a non-destructive bit-wise merge of this bit field and another one.
+ *
+ * @param other the other bit field
+ * @return this bit field
+ */
+ public BitField or(BitField other) {
+ byte[] largerArray, smallerArray;
+ if (bytes.length < other.bytes.length) {
+ largerArray = other.bytes;
+ smallerArray = bytes;
+ } else {
+ largerArray = bytes;
+ smallerArray = other.bytes;
+ }
+
+ // Start out with a copy of the larger of the two arrays.
+ byte[] result = Arrays.copyOf(largerArray, largerArray.length);
+
+ for (int i = 0; i < smallerArray.length; i++) {
+ result[i] |= smallerArray[i];
+ }
+
+ return new BitField(result);
+ }
+
+ /**
+ * Compares two bit fields for equality.
+ *
+ * @param obj another object
+ * @return true if the other object is a bit field with the same bits set
+ */
+ @Override
+ public boolean equals(Object obj) {
+ return EqualsBuilder.reflectionEquals(this, obj);
+ }
+
+ /**
+ * Compare a BitField object with an array of bytes
+ *
+ * @param other a byte array to compare to
+ * @return true if the underlying byte array is equal to the given byte array
+ */
+ public boolean equals(byte[] other) {
+ return Arrays.equals(bytes, other);
+ }
+
+ @Override
+ public int hashCode() {
+ return HashCodeBuilder.reflectionHashCode(this);
+ }
+
+ public int countBitsSet() {
+ int count = 0;
+ for (byte b : bytes) {
+ // JAVA doesn't have the concept of unsigned byte; need to & with 255
+ // to avoid exception of IndexOutOfBoundException when b < 0.
+ count += BIT_COUNT_LOOKUP[0xFF & b];
+ }
+ return count;
+ }
+
+ public BitField not() {
+ byte[] invertedBytes = new byte[bytes.length];
+ for (int i = 0; i < bytes.length; i++) {
+ invertedBytes[i] = Integer.valueOf(~bytes[i]).byteValue();
+ }
+ return new BitField(invertedBytes);
+ }
+
+ public int sizeInBits() {
+ return bytes.length * 8;
+ }
+
+ public boolean any() {
+ for (int i = 0; i < bytes.length; i++) {
+ if (bytes[i] != (byte) 0) {
+ return true;
+ }
+ }
+ return false;
+ }
+}
diff --git a/src/java_tools/junitrunner/java/com/google/testing/coverage/BranchCoverageDetail.java b/src/java_tools/junitrunner/java/com/google/testing/coverage/BranchCoverageDetail.java
new file mode 100644
index 0000000000..6b56a1cff8
--- /dev/null
+++ b/src/java_tools/junitrunner/java/com/google/testing/coverage/BranchCoverageDetail.java
@@ -0,0 +1,86 @@
+// 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.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.TreeSet;
+
+/** Details of branch coverage information. */
+public class BranchCoverageDetail {
+ private final Map<Integer, BitField> branchTaken;
+ private final Map<Integer, Integer> branches;
+
+ public BranchCoverageDetail() {
+ branchTaken = new TreeMap<Integer, BitField>();
+ branches = new TreeMap<Integer, Integer>();
+ }
+
+ private BitField getBranchForLine(int line) {
+ BitField value = branchTaken.get(line);
+ if (value != null) {
+ return value;
+ }
+ value = new BitField();
+ branchTaken.put(line, value);
+ return value;
+ }
+
+ /** Returns true if the line has branches. */
+ public boolean hasBranches(int line) {
+ return branches.containsKey(line);
+ }
+
+ /** Sets the number of branches entry. */
+ public void setBranches(int line, int n) {
+ branches.put(line, n);
+ }
+
+ /** Gets the number of branches in the line, returns 0 if there is no branch. */
+ public int getBranches(int line) {
+ Integer value = branches.get(line);
+ if (value == null) {
+ return 0;
+ }
+ return value;
+ }
+
+ /** Sets the taken bit of the given line for the given branch index. */
+ public void setTakenBit(int line, int branchIdx) {
+ getBranchForLine(line).setBit(branchIdx);
+ }
+
+ public boolean getTakenBit(int line, int branchIdx) {
+ return getBranchForLine(line).isBitSet(branchIdx);
+ }
+
+ /** Calculate executed bit using heuristics. */
+ public boolean getExecutedBit(int line) {
+ // If any of the branch is taken, the branch must have executed. Otherwise assume it is not.
+ return getBranchForLine(line).any();
+ }
+
+ /** Returns line numbers where more than one branch is present. */
+ public Set<Integer> linesWithBranches() {
+ Set<Integer> result = new TreeSet<Integer>();
+ for (int i : branches.keySet()) {
+ if (branches.get(i) > 1) {
+ result.add(i);
+ }
+ }
+ return result;
+ }
+}
diff --git a/src/java_tools/junitrunner/java/com/google/testing/coverage/BranchDetailAnalyzer.java b/src/java_tools/junitrunner/java/com/google/testing/coverage/BranchDetailAnalyzer.java
new file mode 100644
index 0000000000..6e85b9fe26
--- /dev/null
+++ b/src/java_tools/junitrunner/java/com/google/testing/coverage/BranchDetailAnalyzer.java
@@ -0,0 +1,86 @@
+// 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.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+import org.jacoco.core.analysis.Analyzer;
+import org.jacoco.core.data.ExecutionData;
+import org.jacoco.core.data.ExecutionDataStore;
+import org.jacoco.core.internal.data.CRC64;
+import org.jacoco.core.internal.flow.ClassProbesAdapter;
+import org.objectweb.asm.ClassReader;
+
+/**
+ * Analyzer that process the branch coverage detail information.
+ *
+ * <p>Reuse the Analyzer class from Jacoco to avoid duplicating the content detection logic.
+ * Override the main {@code Analyzer.analyzeClass} method which does the main work.
+ */
+public class BranchDetailAnalyzer extends Analyzer {
+
+ private final ExecutionDataStore executionData;
+ private final Map<String, BranchCoverageDetail> branchDetails;
+
+ public BranchDetailAnalyzer(final ExecutionDataStore executionData) {
+ super(executionData, null);
+ this.executionData = executionData;
+ this.branchDetails = new TreeMap<String, BranchCoverageDetail>();
+ }
+
+ @Override
+ public void analyzeClass(final ClassReader reader) {
+ final Map<Integer, BranchExp> lineToBranchExp = mapProbes(reader);
+
+ long classid = CRC64.checksum(reader.b);
+ ExecutionData classData = executionData.get(classid);
+ if (classData == null) {
+ return;
+ }
+ boolean[] probes = classData.getProbes();
+
+ BranchCoverageDetail detail = new BranchCoverageDetail();
+
+ for (Map.Entry<Integer, BranchExp> entry : lineToBranchExp.entrySet()) {
+ int line = entry.getKey();
+ BranchExp branchExp = entry.getValue();
+ List<CovExp> branches = branchExp.getBranches();
+
+ detail.setBranches(line, branches.size());
+ for (int branchIdx = 0; branchIdx < branches.size(); branchIdx++) {
+ if (branches.get(branchIdx).eval(probes)) {
+ detail.setTakenBit(line, branchIdx);
+ }
+ }
+ }
+ if (detail.linesWithBranches().size() > 0) {
+ branchDetails.put(reader.getClassName(), detail);
+ }
+ }
+
+ // Generate the line to probeExp map so that we can evaluate the coverage.
+ private Map<Integer, BranchExp> mapProbes(final ClassReader reader) {
+ final ClassProbesMapper mapper = new ClassProbesMapper();
+ final ClassProbesAdapter adapter = new ClassProbesAdapter(mapper, false);
+ reader.accept(adapter, 0);
+
+ return mapper.result();
+ }
+
+ public Map<String, BranchCoverageDetail> getBranchDetails() {
+ return branchDetails;
+ }
+}
diff --git a/src/java_tools/junitrunner/java/com/google/testing/coverage/BranchExp.java b/src/java_tools/junitrunner/java/com/google/testing/coverage/BranchExp.java
new file mode 100644
index 0000000000..f6b1f9be7c
--- /dev/null
+++ b/src/java_tools/junitrunner/java/com/google/testing/coverage/BranchExp.java
@@ -0,0 +1,81 @@
+// 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.util.ArrayList;
+import java.util.List;
+
+/** A branch coverage that must be evaluated as a combination of probes. */
+public class BranchExp implements CovExp {
+ private final List<CovExp> branches;
+
+ private boolean hasValue;
+ private boolean[] probesUsed;
+ private boolean value;
+
+ public BranchExp(List<CovExp> branches) {
+ this.branches = branches;
+ hasValue = false;
+ }
+
+ /** Create a new BranchExp using this CovExp as the only branch. */
+ public BranchExp(CovExp exp) {
+ branches = new ArrayList<CovExp>();
+ branches.add(exp);
+ hasValue = false;
+ }
+
+ /** Gets the expressions for the branches. */
+ public List<CovExp> getBranches() {
+ return branches;
+ }
+
+ /**
+ * Add an expression to a branch expression.
+ *
+ * @return the index of the newly added branch.
+ */
+ public int add(CovExp exp) {
+ branches.add(exp);
+ return branches.size() - 1;
+ }
+
+ /** Update an existing branch expression. */
+ public void update(int idx, CovExp exp) {
+ branches.set(idx, exp);
+ }
+
+ /** Make a union of the the branches of two BranchExp. */
+ public void merge(BranchExp other) {
+ branches.addAll(other.branches);
+ }
+
+ @Override
+ public boolean eval(final boolean[] probes) {
+ if (hasValue && probes == probesUsed) {
+ return value;
+ }
+ value = false;
+ for (CovExp exp : branches) {
+ value = exp.eval(probes);
+ if (value) {
+ break;
+ }
+ }
+ hasValue = value; // The value is cached.
+ probesUsed = probes;
+ return value;
+ }
+}
diff --git a/src/java_tools/junitrunner/java/com/google/testing/coverage/ClassProbesMapper.java b/src/java_tools/junitrunner/java/com/google/testing/coverage/ClassProbesMapper.java
new file mode 100644
index 0000000000..ed65bf184b
--- /dev/null
+++ b/src/java_tools/junitrunner/java/com/google/testing/coverage/ClassProbesMapper.java
@@ -0,0 +1,59 @@
+// 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.util.Map;
+import java.util.TreeMap;
+import org.jacoco.core.internal.flow.ClassProbesVisitor;
+import org.jacoco.core.internal.flow.MethodProbesVisitor;
+import org.objectweb.asm.FieldVisitor;
+
+/** A visitor that maps each source code line to the probes corresponding to the lines. */
+public class ClassProbesMapper extends ClassProbesVisitor {
+ private Map<Integer, BranchExp> classLineToBranchExp;
+
+ public Map<Integer, BranchExp> result() {
+ return classLineToBranchExp;
+ }
+
+ /** Create a new probe mapper object. */
+ public ClassProbesMapper() {
+ classLineToBranchExp = new TreeMap<Integer, BranchExp>();
+ }
+
+ /** Returns a visitor for mapping method code. */
+ @Override
+ public MethodProbesVisitor visitMethod(
+ int access, String name, String desc, String signature, String[] exceptions) {
+ return new MethodProbesMapper() {
+ @Override
+ public void visitEnd() {
+ super.visitEnd();
+ classLineToBranchExp.putAll(result());
+ }
+ };
+ }
+
+ @Override
+ public FieldVisitor visitField(
+ int access, String name, String desc, String signature, Object value) {
+ return super.visitField(access, name, desc, signature, value);
+ }
+
+ @Override
+ public void visitTotalProbeCount(int count) {
+ // Nothing to do. Maybe perform some sanity checks here.
+ }
+}
diff --git a/src/java_tools/junitrunner/java/com/google/testing/coverage/CovExp.java b/src/java_tools/junitrunner/java/com/google/testing/coverage/CovExp.java
new file mode 100644
index 0000000000..d5a56a5351
--- /dev/null
+++ b/src/java_tools/junitrunner/java/com/google/testing/coverage/CovExp.java
@@ -0,0 +1,24 @@
+// 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;
+
+/**
+ * Expressions to evaluate a branch coverage result, either a simple probeId or a combination of
+ * probes.
+ */
+public interface CovExp {
+ /** Evaluate the expression using the given values of probes. */
+ public abstract boolean eval(final boolean[] probes);
+}
diff --git a/src/java_tools/junitrunner/java/com/google/testing/coverage/IllegalStringException.java b/src/java_tools/junitrunner/java/com/google/testing/coverage/IllegalStringException.java
new file mode 100644
index 0000000000..7d6a7d8c54
--- /dev/null
+++ b/src/java_tools/junitrunner/java/com/google/testing/coverage/IllegalStringException.java
@@ -0,0 +1,26 @@
+// 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;
+
+/**
+ * Thrown when BitField.fromString() is called with a string with either odd number of or
+ * non-hexadecimal characters
+ */
+public class IllegalStringException extends RuntimeException {
+
+ public IllegalStringException(String message) {
+ super(message);
+ }
+}
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 0000000000..3ef86a006f
--- /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});
+ }
+}
diff --git a/src/java_tools/junitrunner/java/com/google/testing/coverage/JacocoLCOVFormatter.java b/src/java_tools/junitrunner/java/com/google/testing/coverage/JacocoLCOVFormatter.java
new file mode 100644
index 0000000000..8c991a11d0
--- /dev/null
+++ b/src/java_tools/junitrunner/java/com/google/testing/coverage/JacocoLCOVFormatter.java
@@ -0,0 +1,151 @@
+// 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.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+import org.jacoco.core.analysis.IBundleCoverage;
+import org.jacoco.core.analysis.IClassCoverage;
+import org.jacoco.core.analysis.ICounter;
+import org.jacoco.core.analysis.IMethodCoverage;
+import org.jacoco.core.analysis.IPackageCoverage;
+import org.jacoco.core.analysis.ISourceFileCoverage;
+import org.jacoco.core.data.ExecutionData;
+import org.jacoco.core.data.SessionInfo;
+import org.jacoco.report.IReportGroupVisitor;
+import org.jacoco.report.IReportVisitor;
+import org.jacoco.report.ISourceFileLocator;
+
+/**
+ * Simple lcov formatter to be used with lcov_merger.par.
+ *
+ * <p>The lcov format is documented here: http://ltp.sourceforge.net/coverage/lcov/geninfo.1.php
+ */
+public class JacocoLCOVFormatter {
+
+ public IReportVisitor createVisitor(
+ final File output, final Map<String, BranchCoverageDetail> branchCoverageDetail) {
+ return new IReportVisitor() {
+
+ private Map<String, Map<String, IClassCoverage>> sourceToClassCoverage = new TreeMap<>();
+ private Map<String, ISourceFileCoverage> sourceToFileCoverage = new TreeMap<>();
+
+ @Override
+ public void visitInfo(List<SessionInfo> sessionInfos, Collection<ExecutionData> executionData)
+ throws IOException {}
+
+ @Override
+ public void visitEnd() throws IOException {
+ try (FileWriter fileWriter = new FileWriter(output, true);
+ PrintWriter printWriter = new PrintWriter(fileWriter)) {
+ for (String sourceFile : sourceToClassCoverage.keySet()) {
+ processSourceFile(printWriter, sourceFile);
+ }
+ }
+ }
+
+ @Override
+ public void visitBundle(IBundleCoverage bundle, ISourceFileLocator locator)
+ throws IOException {
+ // Jacoco's API is geared towards HTML/XML reports which have a hierarchical nature. The
+ // following loop would call the report generators for packages, classes, methods, and
+ // finally link the source view (which would be generated by walking the actual source file
+ // and annotating the coverage data). For lcov, we don't really need the source file, but
+ // we need to output FN/FNDA pairs with method coverage, which means we need to index this
+ // information and process everything at the end.
+ for (IPackageCoverage pkgCoverage : bundle.getPackages()) {
+ for (IClassCoverage clsCoverage : pkgCoverage.getClasses()) {
+ String fileName = clsCoverage.getPackageName() + "/" + clsCoverage.getSourceFileName();
+ if (!sourceToClassCoverage.containsKey(fileName)) {
+ sourceToClassCoverage.put(fileName, new TreeMap<String, IClassCoverage>());
+ }
+ sourceToClassCoverage.get(fileName).put(clsCoverage.getName(), clsCoverage);
+ }
+ for (ISourceFileCoverage srcCoverage : pkgCoverage.getSourceFiles()) {
+ sourceToFileCoverage.put(
+ srcCoverage.getPackageName() + "/" + srcCoverage.getName(), srcCoverage);
+ }
+ }
+ }
+
+ @Override
+ public IReportGroupVisitor visitGroup(String name) throws IOException {
+ return null;
+ }
+
+ private void processSourceFile(PrintWriter writer, String sourceFile) {
+ writer.printf("SF:%s\n", sourceFile);
+
+ ISourceFileCoverage srcCoverage = sourceToFileCoverage.get(sourceFile);
+ if (srcCoverage != null) {
+ // List methods, including methods from nested classes, in FN/FNDA pairs
+ for (IClassCoverage clsCoverage : sourceToClassCoverage.get(sourceFile).values()) {
+ for (IMethodCoverage mthCoverage : clsCoverage.getMethods()) {
+ String name = constructFunctionName(mthCoverage, clsCoverage.getName());
+ writer.printf("FN:%d,%s\n", mthCoverage.getFirstLine(), name);
+ writer.printf("FNDA:%d,%s\n", mthCoverage.getMethodCounter().getCoveredCount(), name);
+ }
+ }
+
+ for (IClassCoverage clsCoverage : sourceToClassCoverage.get(sourceFile).values()) {
+ BranchCoverageDetail detail = branchCoverageDetail.get(clsCoverage.getName());
+ if (detail != null) {
+ for (int line : detail.linesWithBranches()) {
+ int numBranches = detail.getBranches(line);
+ boolean executed = detail.getExecutedBit(line);
+ if (executed) {
+ for (int branchIdx = 0; branchIdx < numBranches; branchIdx++) {
+ if (detail.getTakenBit(line, branchIdx)) {
+ writer.printf("BA:%d,%d\n", line, 2); // executed, taken
+ } else {
+ writer.printf("BA:%d,%d\n", line, 1); // executed, not taken
+ }
+ }
+ } else {
+ for (int branchIdx = 0; branchIdx < numBranches; branchIdx++) {
+ writer.printf("BA:%d,%d\n", line, 0); // not executed
+ }
+ }
+ }
+ }
+ }
+
+ // List of DA entries matching source lines
+ int firstLine = srcCoverage.getFirstLine();
+ int lastLine = srcCoverage.getLastLine();
+ for (int line = firstLine; line <= lastLine; line++) {
+ ICounter instructionCounter = srcCoverage.getLine(line).getInstructionCounter();
+ if (instructionCounter.getTotalCount() != 0) {
+ writer.printf("DA:%d,%d\n", line, instructionCounter.getCoveredCount());
+ }
+ }
+ }
+ writer.println("end_of_record");
+ }
+
+ private String constructFunctionName(IMethodCoverage mthCoverage, String clsName) {
+ // The lcov spec doesn't of course cover Java formats, so we output the method signature.
+ // lcov_merger doesn't seem to care about these entries.
+ return clsName + "::" + mthCoverage.getName() + " " + mthCoverage.getDesc();
+ }
+ };
+ }
+}
diff --git a/src/java_tools/junitrunner/java/com/google/testing/coverage/MethodProbesMapper.java b/src/java_tools/junitrunner/java/com/google/testing/coverage/MethodProbesMapper.java
new file mode 100644
index 0000000000..6900a37f71
--- /dev/null
+++ b/src/java_tools/junitrunner/java/com/google/testing/coverage/MethodProbesMapper.java
@@ -0,0 +1,403 @@
+// 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.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+import org.jacoco.core.internal.flow.IFrame;
+import org.jacoco.core.internal.flow.Instruction;
+import org.jacoco.core.internal.flow.LabelInfo;
+import org.jacoco.core.internal.flow.MethodProbesVisitor;
+import org.objectweb.asm.Handle;
+import org.objectweb.asm.Label;
+
+/**
+ * The mapper is a probes visitor that will cache control flow information as well as keeping track
+ * of the probes as the main driver generates the probe ids. Upon finishing the method it uses the
+ * information collected to generate the mapping information between probes and the instructions.
+ */
+public class MethodProbesMapper extends MethodProbesVisitor {
+ /*
+ * The implementation roughly follows the same pattern of the Analyzer class of Jacoco.
+ *
+ * The mapper has a few states:
+ *
+ * - lineMappings: a mapping between line number and labels
+ *
+ * - a sequence of "instructions", where each instruction has one or more predecessors. The
+ * predecessor field has a sole purpose of propagating probe id. The 'merge' nodes in the CFG has
+ * no predecessors, since the branch stops at theses points.
+ *
+ * - The instructions each has states that keep track of the probes that are associated with the
+ * instruction.
+ *
+ * Initially the probe ids are assigned to the instructions that immediately precede the probe. At
+ * the end of visiting the methods, the probe ids are propagated through the predecessor chains.
+ */
+
+ // States
+ //
+ // These are state variables that needs to be updated in the visitor methods.
+ // The values usually changes as we traverse the byte code.
+ private Instruction lastInstruction = null;
+ private int currentLine = -1;
+ private List<Label> currentLabels = new ArrayList<>();
+
+ // Result
+ private Map<Integer, BranchExp> lineToBranchExp = new TreeMap();
+
+ public Map<Integer, BranchExp> result() {
+ return lineToBranchExp;
+ }
+
+ // Intermediate results
+ //
+ // These values are built up during the visitor methods. They will be used to compute
+ // the final results.
+ private List<Instruction> instructions = new ArrayList<Instruction>();
+ private List<Jump> jumps = new ArrayList<>();
+ private Map<Integer, Instruction> probeToInsn = new TreeMap<>();
+
+ // A map which associates intructions with their coverage expressions.
+ private final Map<Instruction, CovExp> insnToCovExp = new HashMap();
+
+ // A map which associates a instruction to the branch index in its predecessor
+ // e.g., the instruction that follows a conditional jump instruction must exists in
+ // this map.
+ private final Map<Instruction, Integer> insnToIdx = new HashMap();
+
+ // Local cache
+ //
+ // These are maps corresponding to data structures available in JaCoCo in other form.
+ // We use local data structure to avoid need to change the JaCoCo internal code.
+ private Map<Instruction, Instruction> predecessors = new HashMap<>();
+ private Map<Label, Instruction> labelToInsn = new HashMap<>();
+
+ /** Visitor method to append a new Instruction */
+ private void visitInsn() {
+ Instruction instruction = new Instruction(currentLine);
+ instructions.add(instruction);
+ if (lastInstruction != null) {
+ instruction.setPredecessor(lastInstruction); // Update branch of lastInstruction
+ predecessors.put(instruction, lastInstruction); // Update local cache
+ }
+
+ for (Label label : currentLabels) {
+ labelToInsn.put(label, instruction);
+ }
+ currentLabels.clear(); // Update states
+ lastInstruction = instruction;
+ }
+
+ // Plain visitors: called from adapter when no probe is needed
+ @Override
+ public void visitInsn(int opcode) {
+ visitInsn();
+ }
+
+ @Override
+ public void visitIntInsn(int opcode, int operand) {
+ visitInsn();
+ }
+
+ @Override
+ public void visitVarInsn(int opcode, int variable) {
+ visitInsn();
+ }
+
+ @Override
+ public void visitTypeInsn(int opcode, String type) {
+ visitInsn();
+ }
+
+ @Override
+ public void visitFieldInsn(int opcode, String owner, String name, String desc) {
+ visitInsn();
+ }
+
+ @Override
+ public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) {
+ visitInsn();
+ }
+
+ @Override
+ public void visitInvokeDynamicInsn(String name, String desc, Handle handle, Object... args) {
+ visitInsn();
+ }
+
+ @Override
+ public void visitLdcInsn(Object cst) {
+ visitInsn();
+ }
+
+ @Override
+ public void visitIincInsn(int var, int inc) {
+ visitInsn();
+ }
+
+ @Override
+ public void visitMultiANewArrayInsn(String desc, int dims) {
+ visitInsn();
+ }
+
+ // Methods that need to update the states
+ @Override
+ public void visitJumpInsn(int opcode, Label label) {
+ visitInsn();
+ jumps.add(new Jump(lastInstruction, label));
+ }
+
+ @Override
+ public void visitLabel(Label label) {
+ currentLabels.add(label);
+ if (!LabelInfo.isSuccessor(label)) {
+ lastInstruction = null;
+ }
+ }
+
+ @Override
+ public void visitLineNumber(int line, Label start) {
+ currentLine = line;
+ }
+
+ /** Visit a switch instruction with no probes */
+ private void visitSwitchInsn(Label dflt, Label[] labels) {
+ visitInsn();
+
+ // Handle default transition
+ LabelInfo.resetDone(dflt);
+ jumps.add(new Jump(lastInstruction, dflt));
+ LabelInfo.setDone(dflt);
+
+ // Handle other transitions
+ LabelInfo.resetDone(labels);
+ for (Label label : labels) {
+ if (!LabelInfo.isDone(label)) {
+ jumps.add(new Jump(lastInstruction, label));
+ LabelInfo.setDone(label);
+ }
+ }
+ }
+
+ @Override
+ public void visitTableSwitchInsn(int min, int max, Label dflt, Label... labels) {
+ visitSwitchInsn(dflt, labels);
+ }
+
+ @Override
+ public void visitLookupSwitchInsn(Label dflt, int[] keys, Label[] labels) {
+ visitSwitchInsn(dflt, labels);
+ }
+
+ private void addProbe(int probeId) {
+ // We do not add probes to the flow graph, but we need to update
+ // the branch count of the predecessor of the probe
+ lastInstruction.addBranch();
+ probeToInsn.put(probeId, lastInstruction);
+ }
+
+ // Probe visit methods
+ @Override
+ public void visitProbe(int probeId) {
+ // This function is only called when visiting a merge node which
+ // is a successor.
+ // It adds an probe point to the last instruction
+ assert (lastInstruction != null);
+
+ addProbe(probeId);
+ lastInstruction = null; // Merge point should have no predecessor.
+ }
+
+ @Override
+ public void visitJumpInsnWithProbe(int opcode, Label label, int probeId, IFrame frame) {
+ visitInsn();
+ addProbe(probeId);
+ }
+
+ @Override
+ public void visitInsnWithProbe(int opcode, int probeId) {
+ visitInsn();
+ addProbe(probeId);
+ }
+
+ @Override
+ public void visitTableSwitchInsnWithProbes(
+ int min, int max, Label dflt, Label[] labels, IFrame frame) {
+ visitSwitchInsnWithProbes(dflt, labels);
+ }
+
+ @Override
+ public void visitLookupSwitchInsnWithProbes(
+ Label dflt, int[] keys, Label[] labels, IFrame frame) {
+ visitSwitchInsnWithProbes(dflt, labels);
+ }
+
+ private void visitSwitchInsnWithProbes(Label dflt, Label[] labels) {
+ visitInsn();
+ LabelInfo.resetDone(dflt);
+ LabelInfo.resetDone(labels);
+
+ visitTargetWithProbe(dflt);
+ for (Label l : labels) {
+ visitTargetWithProbe(l);
+ }
+ }
+
+ private void visitTargetWithProbe(Label label) {
+ if (!LabelInfo.isDone(label)) {
+ int id = LabelInfo.getProbeId(label);
+ if (id == LabelInfo.NO_PROBE) {
+ jumps.add(new Jump(lastInstruction, label));
+ } else {
+ // Note, in this case the instrumenter should insert intermediate labels
+ // for the probes. These probes will be added for the switch instruction.
+ //
+ // There is no direct jump between lastInstruction and the label either.
+ addProbe(id);
+ }
+ LabelInfo.setDone(label);
+ }
+ }
+
+ // If a CovExp of pred is ProbeExp, create a single-branch BranchExp and put it in the map.
+ // Also update the index of insn.
+ private BranchExp getPredBranchExp(Instruction predecessor, Instruction insn) {
+ BranchExp result = null;
+ CovExp exp = insnToCovExp.get(predecessor);
+ if (exp instanceof ProbeExp) {
+ result = new BranchExp(exp); // Change ProbeExp to BranchExp
+ insnToCovExp.put(predecessor, result);
+ // This can only happen if the internal data of Jacoco is inconsistent:
+ // the instruction is the predecessor of more than one instructions,
+ // but its branch count is not > 1.
+ } else {
+ result = (BranchExp) exp;
+ }
+ return result;
+ }
+
+ // Update a branch predecessor and returns whether the BranchExp of the predecessor is new.
+ private boolean updateBranchPredecessor(Instruction predecessor, Instruction insn, CovExp exp) {
+ CovExp predExp = insnToCovExp.get(predecessor);
+ if (predExp == null) {
+ BranchExp branchExp = new BranchExp(exp);
+ insnToCovExp.put(predecessor, branchExp);
+ insnToIdx.put(insn, 0); // current insn is the first branch
+ return true;
+ }
+
+ BranchExp branchExp = getPredBranchExp(predecessor, insn);
+ Integer branchIdx = insnToIdx.get(insn);
+ if (branchIdx == null) {
+ // Keep track of the instructions in the branches that are already added
+ branchIdx = branchExp.add(exp);
+ insnToIdx.put(insn, branchIdx);
+ }
+ // If the branch where the instruction is on is already added, no need to do anything as
+ // branchExp has a reference to exp already.
+ return false;
+ }
+
+ /** Finishing the method */
+ @Override
+ public void visitEnd() {
+
+ for (Jump jump : jumps) {
+ Instruction insn = labelToInsn.get(jump.target);
+ insn.setPredecessor(jump.source);
+ predecessors.put(insn, jump.source);
+ }
+
+ // Compute CovExp for every instruction.
+ for (Map.Entry<Integer, Instruction> entry : probeToInsn.entrySet()) {
+ int probeId = entry.getKey();
+ Instruction ins = entry.getValue();
+
+ Instruction insn = ins;
+ CovExp exp = new ProbeExp(probeId);
+
+ // Compute CovExp for the probed instruction.
+ CovExp existingExp = insnToCovExp.get(insn);
+ if (existingExp != null) {
+ // The instruction already has a branch, add the probeExp as
+ // a new branch.
+ if (existingExp instanceof BranchExp) {
+ BranchExp branchExp = (BranchExp) existingExp;
+ branchExp.add(exp);
+ } else {
+ // This can only happen if the internal data is inconsistent.
+ // The instruction is a predecessor of another instruction and also
+ // has a probe, but the branch count is not > 1.
+ }
+ } else {
+ if (insn.getBranches() > 1) {
+ exp = new BranchExp(exp);
+ }
+ insnToCovExp.put(insn, exp);
+ }
+
+ Instruction predecessor = predecessors.get(insn);
+ while (predecessor != null) {
+ if (predecessor.getBranches() > 1) {
+ boolean isNewBranch = updateBranchPredecessor(predecessor, insn, exp);
+ if (!isNewBranch) {
+ // If the branch already exists, no need to visit predecessors any more.
+ break;
+ }
+ } else {
+ // No branch at predecessor, use the same CovExp
+ insnToCovExp.put(predecessor, exp);
+ }
+ insn = predecessor;
+ exp = insnToCovExp.get(predecessor);
+ predecessor = predecessors.get(insn);
+ }
+ }
+
+ // Merge branches in the instructions on the same line
+ for (Instruction insn : instructions) {
+ if (insn.getBranches() > 1) {
+ CovExp insnExp = insnToCovExp.get(insn);
+ if (insnExp != null && (insnExp instanceof BranchExp)) {
+ BranchExp exp = (BranchExp) insnExp;
+ BranchExp lineExp = lineToBranchExp.get(insn.getLine());
+ if (lineExp == null) {
+ lineToBranchExp.put(insn.getLine(), exp);
+ } else {
+ lineExp.merge(exp);
+ }
+ } else {
+ // If we reach here, the internal data of the mapping is inconsistent, either
+ // 1) An instruction has branches but we do not create BranchExp for it.
+ // 2) An instruction has branches but it does not have an associated CovExp.
+ }
+ }
+ }
+ }
+
+ /** Jumps between instructions and labels */
+ class Jump {
+ public final Instruction source;
+ public final Label target;
+
+ public Jump(Instruction i, Label l) {
+ source = i;
+ target = l;
+ }
+ }
+}
diff --git a/src/java_tools/junitrunner/java/com/google/testing/coverage/ProbeExp.java b/src/java_tools/junitrunner/java/com/google/testing/coverage/ProbeExp.java
new file mode 100644
index 0000000000..888b6c7a55
--- /dev/null
+++ b/src/java_tools/junitrunner/java/com/google/testing/coverage/ProbeExp.java
@@ -0,0 +1,29 @@
+// 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;
+
+/** The branch coverage can be evaluated by looking up single probe value. */
+public class ProbeExp implements CovExp {
+ private final int probeId;
+
+ public ProbeExp(int id) {
+ probeId = id;
+ }
+
+ @Override
+ public boolean eval(final boolean[] probes) {
+ return probes[probeId];
+ }
+}