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

--
PiperOrigin-RevId: 141046146
MOS_MIGRATED_REVID=141046146
diff --git a/src/java_tools/junitrunner/java/com/google/testing/coverage/BUILD b/src/java_tools/junitrunner/java/com/google/testing/coverage/BUILD
new file mode 100644
index 0000000..863bcc0
--- /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 0000000..c52cacc
--- /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 0000000..5967677
--- /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 0000000..6b56a1c
--- /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 0000000..6e85b9f
--- /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 0000000..f6b1f9b
--- /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 0000000..ed65bf1
--- /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 0000000..d5a56a5
--- /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 0000000..7d6a7d8
--- /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 0000000..3ef86a0
--- /dev/null
+++ b/src/java_tools/junitrunner/java/com/google/testing/coverage/JacocoCoverageRunner.java
@@ -0,0 +1,278 @@
+// Copyright 2016 The Bazel Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.testing.coverage;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.Reader;
+import java.lang.reflect.Method;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+import java.util.jar.Attributes;
+import java.util.jar.JarInputStream;
+import java.util.jar.Manifest;
+import org.jacoco.agent.rt.IAgent;
+import org.jacoco.agent.rt.RT;
+import org.jacoco.core.analysis.Analyzer;
+import org.jacoco.core.analysis.CoverageBuilder;
+import org.jacoco.core.analysis.IBundleCoverage;
+import org.jacoco.core.tools.ExecFileLoader;
+import org.jacoco.report.IReportVisitor;
+import org.jacoco.report.ISourceFileLocator;
+
+/**
+ * Runner class used to generate code coverage report when using Jacoco offline instrumentation.
+ *
+ * <p>The complete list of features available for Jacoco offline instrumentation:
+ * http://www.eclemma.org/jacoco/trunk/doc/offline.html
+ *
+ * <p>The structure is roughly following the canonical Jacoco example:
+ * http://www.eclemma.org/jacoco/trunk/doc/examples/java/ReportGenerator.java
+ *
+ * <p>The following environment variables are expected:
+ * JAVA_COVERAGE_FILE - specifies final location of the generated lcov file.
+ * JACOCO_METADATA_JAR - specifies jar containing uninstrumented classes to be analyzed.
+ */
+public class JacocoCoverageRunner {
+
+  private final List<File> classesJars;
+  private final InputStream executionData;
+  private final File reportFile;
+  private ExecFileLoader execFileLoader;
+
+  public JacocoCoverageRunner(InputStream jacocoExec, String reportPath, File... metadataJars) {
+    executionData = jacocoExec;
+    reportFile = new File(reportPath);
+
+    classesJars = new ArrayList<>();
+    for (File metadataJar : metadataJars) {
+      classesJars.add(metadataJar);
+    }
+  }
+
+  public void create() throws IOException {
+    // Read the jacoco.exec file. Multiple data files could be merged at this point
+    execFileLoader = new ExecFileLoader();
+    execFileLoader.load(executionData);
+
+    // Run the structure analyzer on a single class folder or jar file to build up the coverage
+    // model. Typically you would create a bundle for each class folder and each jar you want in
+    // your report. If you have more than one bundle you may need to add a grouping node to the
+    // report. The lcov formatter doesn't seem to care, and we're only using one bundle anyway.
+    final IBundleCoverage bundleCoverage = analyzeStructure();
+
+    final Map<String, BranchCoverageDetail> branchDetails = analyzeBranch();
+    createReport(bundleCoverage, branchDetails);
+  }
+
+  private void createReport(
+      final IBundleCoverage bundleCoverage, final Map<String, BranchCoverageDetail> branchDetails)
+      throws IOException {
+    JacocoLCOVFormatter formatter = new JacocoLCOVFormatter();
+    final IReportVisitor visitor = formatter.createVisitor(reportFile, branchDetails);
+
+    // Initialize the report with all of the execution and session information. At this point the
+    // report doesn't know about the structure of the report being created.
+    visitor.visitInfo(
+        execFileLoader.getSessionInfoStore().getInfos(),
+        execFileLoader.getExecutionDataStore().getContents());
+
+    // Populate the report structure with the bundle coverage information.
+    // Call visitGroup if you need groups in your report.
+
+    // Note the API requires a sourceFileLocator because the HTML and XML formatters display a page
+    // of code annotated with coverage information. Having the source files is not actually needed
+    // for generating the lcov report...
+    visitor.visitBundle(
+        bundleCoverage,
+        new ISourceFileLocator() {
+
+          @Override
+          public Reader getSourceFile(String packageName, String fileName) throws IOException {
+            return null;
+          }
+
+          @Override
+          public int getTabWidth() {
+            return 0;
+          }
+        });
+
+    // Signal end of structure information to allow report to write all information out
+    visitor.visitEnd();
+  }
+
+  private IBundleCoverage analyzeStructure() throws IOException {
+    final CoverageBuilder coverageBuilder = new CoverageBuilder();
+    final Analyzer analyzer = new Analyzer(execFileLoader.getExecutionDataStore(), coverageBuilder);
+    for (File classesJar : classesJars) {
+      analyzer.analyzeAll(classesJar);
+    }
+
+    // TODO(bazel-team): Find out where the name of the bundle can pop out in the report.
+    return coverageBuilder.getBundle("isthisevenused");
+  }
+
+  // Additional pass to process the branch details of the classes
+  private Map<String, BranchCoverageDetail> analyzeBranch() throws IOException {
+    final BranchDetailAnalyzer analyzer =
+        new BranchDetailAnalyzer(execFileLoader.getExecutionDataStore());
+
+    Map<String, BranchCoverageDetail> result = new TreeMap<>();
+    for (File classesJar : classesJars) {
+      analyzer.analyzeAll(classesJar);
+      result.putAll(analyzer.getBranchDetails());
+    }
+    return result;
+  }
+
+  private static String getMainClass(String metadataJar) throws Exception {
+    if (metadataJar != null) {
+      // Blaze guarantees that JACOCO_METADATA_JAR has a proper manifest with a Main-Class entry.
+      try (JarInputStream jarStream = new JarInputStream(new FileInputStream(metadataJar))) {
+        return jarStream.getManifest().getMainAttributes().getValue("Main-Class");
+      }
+    } else {
+      // If metadataJar was not set, we're running inside a deploy jar. We have to open the manifest
+      // and read the value of "Precoverage-Class", set by Blaze. Note ClassLoader#getResource()
+      // will only return the first result, most likely a manifest from the bootclasspath.
+      Enumeration<URL> manifests =
+          JacocoCoverageRunner.class.getClassLoader().getResources("META-INF/MANIFEST.MF");
+      while (manifests.hasMoreElements()) {
+        Manifest manifest = new Manifest(manifests.nextElement().openStream());
+        Attributes attributes = manifest.getMainAttributes();
+        String className = attributes.getValue("Coverage-Main-Class");
+        if (className != null) {
+          return className;
+        }
+      }
+      throw new IllegalStateException(
+          "JACOCO_METADATA_JAR environment variable is not set, and no"
+              + " META-INF/MANIFEST.MF on the classpath has a Coverage-Main-Class attribute. "
+              + " Cannot determine the name of the main class for the code under test.");
+    }
+  }
+
+  private static String getUniquePath(String pathTemplate, String suffix) throws IOException {
+    // If pathTemplate is null, we're likely executing from a deploy jar and the test framework
+    // did not properly set the environment for coverage reporting. This alone is not a reason for
+    // throwing an exception, we're going to run anyway and write the coverage data to a temporary,
+    // throw-away file.
+    if (pathTemplate == null) {
+      return File.createTempFile("coverage", suffix).getPath();
+    } else {
+      // Blaze sets the path template to a file with the .dat extension. lcov_merger matches all
+      // files having '.dat' in their name, so instead of appending we change the extension.
+      File absolutePathTemplate = new File(pathTemplate).getAbsoluteFile();
+      String prefix = absolutePathTemplate.getName();
+      int lastDot = prefix.lastIndexOf('.');
+      if (lastDot != -1) {
+        prefix = prefix.substring(0, lastDot);
+      }
+      return File.createTempFile(prefix, suffix, absolutePathTemplate.getParentFile()).getPath();
+    }
+  }
+
+  public static void main(String[] args) throws Exception {
+    final String metadataJar = System.getenv("JACOCO_METADATA_JAR");
+    final String coverageReportBase = System.getenv("JAVA_COVERAGE_FILE");
+
+    // Disable Jacoco's default output mechanism, which runs as a shutdown hook. We generate the
+    // report in our own shutdown hook below, and we want to avoid the data race (shutdown hooks are
+    // not guaranteed any particular order). Note that also by default, Jacoco appends coverage
+    // data, which can have surprising results if running tests locally or somehow encountering
+    // the previous .exec file.
+    System.setProperty("jacoco-agent.output", "none");
+
+    // We have no use for this sessionId property, but leaving it blank results in a DNS lookup
+    // at runtime. A minor annoyance: the documentation insists the property name is "sessionId",
+    // however on closer inspection of the source code, it turns out to be "sessionid"...
+    System.setProperty("jacoco-agent.sessionid", "default");
+
+    // A JVM shutdown hook has a fixed amount of time (OS-dependent) before it is terminated.
+    // For our purpose, it's more than enough to scan through the instrumented jar and match up
+    // the bytecode with the coverage data. It wouldn't be enough for scanning the entire classpath,
+    // or doing something else terribly inefficient.
+    Runtime.getRuntime()
+        .addShutdownHook(
+            new Thread() {
+              @Override
+              public void run() {
+                try {
+                  // If the test spawns multiple JVMs, they will race to write to the same files. We
+                  // need to generate unique paths for each execution. lcov_merger simply collects
+                  // all the .dat files in the current directory anyway, so we don't need to worry
+                  // about merging them.
+                  String coverageReport = getUniquePath(coverageReportBase, ".dat");
+                  String coverageData = getUniquePath(coverageReportBase, ".exec");
+
+                  // Get a handle on the Jacoco Agent and write out the coverage data. Other options
+                  // included talking to the agent via TCP (useful when gathering coverage from
+                  // multiple JVMs), or via JMX (the agent's MXBean is called
+                  // 'org.jacoco:type=Runtime'). As we're running in the same JVM, these options
+                  // seemed overkill, we can just refer to the Jacoco runtime as RT.
+                  // See http://www.eclemma.org/jacoco/trunk/doc/agent.html for all the options
+                  // available.
+                  ByteArrayInputStream dataInputStream;
+                  try {
+                    IAgent agent = RT.getAgent();
+                    byte[] data = agent.getExecutionData(false);
+                    try (FileOutputStream fs = new FileOutputStream(coverageData, true)) {
+                      fs.write(data);
+                    }
+                    // We append to the output file, but run report generation only for the coverage
+                    // data from this JVM. The output file may contain data from other
+                    // subprocesses, etc.
+                    dataInputStream = new ByteArrayInputStream(data);
+                  } catch (IllegalStateException e) {
+                    // In this case, we didn't execute a single instrumented file, so the agent
+                    // isn't live. There's no coverage to report, but it's otherwise a successful
+                    // invocation.
+                    dataInputStream = new ByteArrayInputStream(new byte[0]);
+                  }
+
+                  if (metadataJar != null) {
+                    // Disable coverage in this case. The build system should report an error or
+                    // warning if this happens. It's too late at this point.
+                    new JacocoCoverageRunner(dataInputStream, coverageReport, new File(metadataJar))
+                        .create();
+                  }
+                } catch (IOException e) {
+                  e.printStackTrace();
+                  Runtime.getRuntime().halt(1);
+                }
+              }
+            });
+
+    // Another option would be to run the tests in a separate JVM, let Jacoco dump out the coverage
+    // data, wait for the subprocess to finish and then generate the lcov report. The only benefit
+    // of doing this is not being constrained by the hard 5s limit of the shutdown hook. Setting up
+    // the subprocess to match all JVM flags, runtime classpath, bootclasspath, etc is doable.
+    // We'd share the same limitation if the system under test uses shutdown hooks internally, as
+    // there's no way to collect coverage data on that code.
+    String mainClass = getMainClass(metadataJar);
+    Method main =
+        Class.forName(mainClass).getMethod("main", new Class[] {java.lang.String[].class});
+    main.invoke(null, new Object[] {args});
+  }
+}
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 0000000..8c991a1
--- /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 0000000..6900a37
--- /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 0000000..888b6c7
--- /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];
+  }
+}