Add --experimental_cc_coverage flag.

This PR adds a new flag `--experimental_cc_coverage` that enables using `gcov` instead of `lcov` for collecting C++ code coverage.

Progress on #5880

Closes #5842.

RELNOTES: Faster coverage collection for gcc compiled C++ code can now be
tested by enabling it with --experimental_cc_coverage.
PiperOrigin-RevId: 213796978
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/config/BuildConfiguration.java b/src/main/java/com/google/devtools/build/lib/analysis/config/BuildConfiguration.java
index 679665f..6e8ed7c 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/config/BuildConfiguration.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/config/BuildConfiguration.java
@@ -589,7 +589,20 @@
     )
     public boolean experimentalJavaCoverage;
 
-
+    @Option(
+        name = "experimental_cc_coverage",
+        defaultValue = "false",
+        documentationCategory = OptionDocumentationCategory.OUTPUT_PARAMETERS,
+        effectTags = {
+          OptionEffectTag.CHANGES_INPUTS,
+          OptionEffectTag.AFFECTS_OUTPUTS,
+          OptionEffectTag.LOADING_AND_ANALYSIS
+        },
+        metadataTags = {OptionMetadataTag.EXPERIMENTAL},
+        help =
+            "If specified, Bazel will use gcov to collect code coverage for C++ test targets. "
+                + "This option only works for gcc compilation.")
+    public boolean useGcovCoverage;
 
     @Option(
       name = "build_runfile_manifests",
@@ -1725,6 +1738,10 @@
     return options.experimentalJavaCoverage;
   }
 
+  public boolean useGcovCoverage() {
+    return options.useGcovCoverage;
+  }
+
   public RunUnder getRunUnder() {
     return options.runUnder;
   }
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/test/TestActionBuilder.java b/src/main/java/com/google/devtools/build/lib/analysis/test/TestActionBuilder.java
index 5a20f52..6742d9f 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/test/TestActionBuilder.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/test/TestActionBuilder.java
@@ -55,6 +55,15 @@
 
   private static final String CC_CODE_COVERAGE_SCRIPT = "CC_CODE_COVERAGE_SCRIPT";
   private static final String LCOV_MERGER = "LCOV_MERGER";
+  // The coverage tool Bazel uses to generate a code coverage report for C++.
+  private static final String BAZEL_CC_COVERAGE_TOOL = "BAZEL_CC_COVERAGE_TOOL";
+
+  enum CcCoverageTool {
+    GCOV,
+    LCOV,
+  }
+
+  private static final CcCoverageTool DEFAULT_BAZEL_CC_COVERAGE_TOOL = CcCoverageTool.LCOV;
 
   private final RuleContext ruleContext;
   private RunfilesSupport runfilesSupport;
@@ -253,6 +262,13 @@
         extraTestEnv.put(CC_CODE_COVERAGE_SCRIPT, collectCcCoverage.getExecPathString());
       }
 
+      // lcov is the default CC coverage tool unless otherwise specified on the command line.
+      extraTestEnv.put(
+          BAZEL_CC_COVERAGE_TOOL,
+          ruleContext.getConfiguration().useGcovCoverage()
+              ? CcCoverageTool.GCOV.toString()
+              : DEFAULT_BAZEL_CC_COVERAGE_TOOL.toString());
+
       // We don't add this attribute to non-supported test target
       if (ruleContext.isAttrDefined("$lcov_merger", LABEL)) {
         TransitiveInfoCollection lcovMerger =
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/rules/cpp/BazelCcTestRule.java b/src/main/java/com/google/devtools/build/lib/bazel/rules/cpp/BazelCcTestRule.java
index e919db1..79ca7ac 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/rules/cpp/BazelCcTestRule.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/rules/cpp/BazelCcTestRule.java
@@ -24,6 +24,7 @@
 import com.google.devtools.build.lib.analysis.RuleDefinitionEnvironment;
 import com.google.devtools.build.lib.analysis.config.HostTransition;
 import com.google.devtools.build.lib.bazel.rules.cpp.BazelCppRuleClasses.CcBinaryBaseRule;
+import com.google.devtools.build.lib.cmdline.Label;
 import com.google.devtools.build.lib.packages.RuleClass;
 import com.google.devtools.build.lib.packages.RuleClass.Builder.RuleClassType;
 import com.google.devtools.build.lib.packages.TriState;
@@ -45,6 +46,11 @@
         .override(attr("linkstatic", BOOLEAN).value(OS.getCurrent() == OS.WINDOWS))
         .override(attr("stamp", TRISTATE).value(TriState.NO))
         .add(
+            attr("$lcov_merger", LABEL)
+                .value(
+                    Label.parseAbsoluteUnchecked(
+                        "@bazel_tools//tools/test/CoverageOutputGenerator/java/com/google/devtools/coverageoutputgenerator:Main")))
+        .add(
             attr("$collect_cc_coverage", LABEL)
                 .cfg(HostTransition.INSTANCE)
                 .singleArtifact()
diff --git a/src/test/shell/bazel/bazel_cc_code_coverage_test.sh b/src/test/shell/bazel/bazel_cc_code_coverage_test.sh
index 0e9b7e3..1382347 100755
--- a/src/test/shell/bazel/bazel_cc_code_coverage_test.sh
+++ b/src/test/shell/bazel/bazel_cc_code_coverage_test.sh
@@ -37,8 +37,6 @@
 readonly ROOT_VAR="${PWD}"
 # Location of the instrumented file manifest.
 readonly COVERAGE_MANIFEST_VAR="${PWD}/coverage_manifest.txt"
-# Location of the final coverage report.
-readonly COVERAGE_OUTPUT_FILE_VAR="${PWD}/coverage_report.dat"
 
 # Path to the canonical C++ coverage script.
 readonly COLLECT_CC_COVERAGE_SCRIPT=tools/test/collect_cc_coverage.sh
@@ -51,7 +49,7 @@
 #
 # - file   The absolute path of the file.
 function get_file_id() {
-  local file="${1}"
+  local file="${1}"; shift
   stat -c "%d:%i" ${file}
 }
 
@@ -69,11 +67,7 @@
 
   # The script expects gcov to be at $COVERAGE_GCOV_PATH.
   cp $( which gcov ) "$COVERAGE_GCOV_PATH_VAR"
-
-  # The script expects the output file to already exist.
-  # TODO(iirina): In the future it would be better if the
-  # script creates the output file.
-  touch "$COVERAGE_OUTPUT_FILE_VAR"
+  mkdir -p "$COVERAGE_DIR_VAR/coverage_srcs"
 
   # All generated .gcno files need to be in the manifest otherwise
   # the coverage report will be incomplete.
@@ -81,12 +75,12 @@
   echo "coverage_srcs/a.gcno" >> "$COVERAGE_MANIFEST_VAR"
 
   # Create the CC sources.
-  mkdir -p coverage_srcs/
-  cat << EOF > coverage_srcs/a.h
+  mkdir -p "$ROOT_VAR/coverage_srcs/"
+  cat << EOF > "$ROOT_VAR/coverage_srcs/a.h"
 int a(bool what);
 EOF
 
-  cat << EOF > coverage_srcs/a.cc
+  cat << EOF > "$ROOT_VAR/coverage_srcs/a.cc"
 #include "a.h"
 
 int a(bool what) {
@@ -98,7 +92,7 @@
 }
 EOF
 
-  cat << EOF > coverage_srcs/t.cc
+  cat << EOF > "$ROOT_VAR/coverage_srcs/t.cc"
 #include <stdio.h>
 #include "a.h"
 
@@ -155,93 +149,182 @@
 function tear_down() {
   rm -f "$COVERAGE_MANIFEST_VAR"
   rm -f "$COVERAGE_GCOV_PATH_VAR"
-  rm -f "$COVERAGE_OUTPUT_FILE_VAR"
   rm -rf "$COVERAGE_DIR_VAR"
   rm -rf coverage_srcs/
 }
 
+# # Usage: run_coverage <coverage_tool>
 # Runs the script that computes the code coverage report for CC code.
 # Sets up the sub-shell environment accordingly:
-# - COVERAGE_DIR            Directory containing gcda files.
-# - COVERAGE_MANIFEST       Location of the instrumented file manifest.
-# - COVERAGE_OUTPUT_FILE    Location of the final coverage report.
-# - COVERAGE_GCOV_PATH      Location of gcov.
-# - ROOT                    Location from where the code coverage collection
-#                           was invoked.
+# COVERAGE_DIR            Directory containing gcda files.
+# COVERAGE_MANIFEST       Location of the instrumented file manifest.
+# COVERAGE_GCOV_PATH      Location of gcov.
+# ROOT                    Location from where the code coverage collection
+#                         was invoked.
+#
+# - coverage_tool         The tool to be used when computing the code
+#                          coverage report. Can be lcov or gcov.
 function run_coverage() {
+   local coverage_tool="${1}"; shift
   (COVERAGE_DIR="$COVERAGE_DIR_VAR" \
    COVERAGE_GCOV_PATH="$COVERAGE_GCOV_PATH_VAR" \
    ROOT="$ROOT_VAR" COVERAGE_MANIFEST="$COVERAGE_MANIFEST_VAR" \
-   COVERAGE_OUTPUT_FILE="$COVERAGE_OUTPUT_FILE_VAR" \
+   BAZEL_CC_COVERAGE_TOOL="$coverage_tool" \
    "$COLLECT_CC_COVERAGE_SCRIPT")
 }
 
-function test_cc_test_coverage() {
+# Asserts if the given expected coverage result is included in the given output
+# file.
+#
+# - expected_coverage The expected result that must be included in the output.
+# - output_file       The location of the coverage output file.
+function assert_coverage_entry_in_file() {
+    local expected_coverage="${1}"; shift
+    local output_file="${1}"; shift
+
+    # Replace newlines with commas to facilitate the assertion.
+    local expected_coverage_no_newlines="$( echo "$expected_coverage" | tr '\n' ',' )"
+    local output_file_no_newlines="$( cat "$output_file" | tr '\n' ',' )"
+
+    (echo "$output_file_no_newlines" | grep  "$expected_coverage_no_newlines")\
+        || fail "Expected coverage result
+<$expected_coverage>
+was not found in actual coverage report:
+<$( cat $output_file )>"
+}
+
+# Asserts if coverage result in lcov format for coverage_srcs/a.cc is included
+# in the given output file.
+#
+# - output_file    The location of the coverage output file.
+function assert_lcov_coverage_srcs_a_cc() {
+    local output_file="${1}"; shift
+
+    # The expected coverage result for coverage_srcs/a.cc in lcov format.
+    local expected_lcov_result_a_cc="TN:
+SF:coverage_srcs/a.cc
+FN:3,_Z1ab
+FNDA:1,_Z1ab
+FNF:1
+FNH:1
+DA:3,1
+DA:4,1
+DA:5,1
+DA:7,0
+LF:4
+LH:3
+end_of_record"
+    assert_coverage_entry_in_file "$expected_lcov_result_a_cc" "$output_file"
+}
+
+# Asserts if coverage result in lcov format for coverage_srcs/t.cc is included
+# in the given output file.
+#
+# - output_file    The location of the coverage output file.
+function assert_lcov_coverage_srcs_t_cc() {
+    local output_file="${1}"; shift
+
+    # The expected coverage result for coverage_srcs/t.cc in lcov format.
+    local expected_lcov_result_t_cc="TN:
+SF:coverage_srcs/t.cc
+FN:4,main
+FNDA:1,main
+FNF:1
+FNH:1
+DA:4,1
+DA:5,1
+DA:6,1
+LF:3
+LH:3
+end_of_record"
+    assert_coverage_entry_in_file "$expected_lcov_result_t_cc" "$output_file"
+}
+
+# Asserts if coverage result in gcov format for coverage_srcs/a.cc is included
+# in the given output file.
+#
+# - output_file    The location of the coverage output file.
+function assert_gcov_coverage_srcs_a_cc() {
+    local output_file="${1}"; shift
+
+    # The expected coverage result for coverage_srcs/a.cc in gcov format.
+    local expected_gcov_result_a_cc="file:coverage_srcs/a.cc
+function:3,1,_Z1ab
+lcount:3,1
+lcount:4,1
+branch:4,taken
+branch:4,nottaken
+lcount:5,1
+lcount:7,0"
+    assert_coverage_entry_in_file "$expected_gcov_result_a_cc" "$output_file"
+}
+
+
+# Asserts if coverage result in gcov format for coverage_srcs/t.cc is included
+# in the given output file.
+#
+# - output_file    The location of the coverage output file.
+function assert_gcov_coverage_srcs_t_cc() {
+    local output_file="${1}"; shift
+
+    # The expected coverage result for coverage_srcs/t.cc in gcov format.
+    local expected_gcov_result_t_cc="file:coverage_srcs/t.cc
+function:4,1,main
+lcount:4,1
+lcount:5,1
+lcount:6,1"
+    assert_coverage_entry_in_file "$expected_gcov_result_t_cc" "$output_file"
+}
+
+function test_cc_test_coverage_lcov() {
     # Run the C++ coverage script with the environment setup accordingly.
     # This will get coverage results for coverage_srcs/a.cc and
     # coverage_srcs/t.cc.
-    run_coverage > "$TEST_log"
+    run_coverage "LCOV" > "$TEST_log"
 
-    # Assert that the C++ coverage output is correct.
-    # The covered files are coverage_srcs/a.cc and coverage_srcs/t.cc.
-
-    # The expected total number of lines of COVERAGE_OUTPUT_FILE
-    # is 25. This number can be computed by counting the number
-    # of lines in the variables declared below $expected_result_a_cc
-    # and $expected_result_t_cc.
-    [[ $(wc -l < "$COVERAGE_OUTPUT_FILE_VAR") == 25 ]] || \
-        fail "Number of lines $nr_of_lines is different than 25"
-
-    # The expected result can be constructed manually by following the lcov
-    # documentation and manually checking what lines of code are covered when
-    # running the test.
-    # For details about the lcov format see
-    # http://ltp.sourceforge.net/coverage/lcov/geninfo.1.php
-    # The newlines are replaced with commas to facilitate comparing the
-    # expected values with the actual results.
-
-    # The expected coverage result for coverage_srcs/a.cc.
-    local expected_result_a_cc="TN:,\
-SF:coverage_srcs/a.cc,\
-FN:3,_Z1ab,\
-FNDA:1,_Z1ab,\
-FNF:1,\
-FNH:1,\
-DA:3,1,\
-DA:4,1,\
-DA:5,1,\
-DA:7,0,\
-LF:4,\
-LH:3,\
-end_of_record"
-
-    # The expected coverage result for coverage_srcs/t.cc.
-    local expected_result_t_cc="TN:,\
-SF:coverage_srcs/t.cc,\
-FN:4,main,\
-FNDA:1,main,\
-FNF:1,\
-FNH:1,\
-DA:4,1,\
-DA:5,1,\
-DA:6,1,\
-LF:3,\
-LH:3,\
-end_of_record"
+    # Location of the output file of the C++ coverage script when lcov is used.
+    local output_file="$COVERAGE_DIR_VAR/_cc_coverage.dat"
 
     # Assert that the coverage output file contains the coverage data for the
     # two cc files: coverage_srcs/a.cc and coverage_srcs/t.cc.
-    # The C++ coverage script places the final coverage result in
-    # $COVERAGE_OUTPUT_FILE.
-    #
     # The result for each source file must be asserted separately because the
-    # coverage tools (e.g. lcov, gcov) do not guarantee any particular order.
+    # coverage lcov does not guarantee any particular order.
     # The order can differ for example based on OS or version. The source files
     # order in the coverage report is not relevant.
-    tr '\n' , < "$COVERAGE_OUTPUT_FILE_VAR" | grep "$expected_result_a_cc" \
-        || fail "Wrong coverage results for coverage_srcs/a.cc"
-    tr '\n' , < "$COVERAGE_OUTPUT_FILE_VAR" | grep "$expected_result_t_cc" \
-        || fail "Wrong coverage results for coverage_srcs/t.cc"
+    assert_lcov_coverage_srcs_a_cc "$output_file"
+    assert_lcov_coverage_srcs_t_cc "$output_file"
+
+    # The expected total number of lines of output file is 25. This assertion
+    # is needed to make sure no other source files are included in the output
+    # file.
+    local nr_lines="$(wc -l < "$output_file")"
+    [[ "$nr_lines" == 25 ]] || \
+      fail "Number of lines in C++ lcov coverage output file is "\
+      "$nr_lines and different than 25"
+}
+
+function test_cc_test_coverage_gcov() {
+    run_coverage "GCOV" > "$TEST_log"
+
+    # Location of the output file of the C++ coverage script when gcov is used.
+    local output_file="$COVERAGE_DIR_VAR/_cc_coverage.gcov"
+
+    # The expected total number of lines of output file is 13. This assertion
+    # is needed to make sure no other source files are included in the output
+    # file.
+    local nr_lines="$(wc -l < "$output_file")"
+    [[ "$nr_lines" == 13 ]] || \
+      fail "Number of lines in C++ gcov coverage output file is "\
+      "$nr_lines and different than 13"
+
+    # Assert that the coverage output file contains the coverage data for the
+    # two cc files: coverage_srcs/a.cc and coverage_srcs/t.cc.
+    # The result for each source file must be asserted separately because the
+    # coverage gcov does not guarantee any particular order.
+    # The order can differ for example based on OS or version. The source files
+    # order in the coverage report is not relevant.
+    assert_gcov_coverage_srcs_a_cc "$output_file"
+    assert_gcov_coverage_srcs_t_cc "$output_file"
 }
 
 run_suite "Testing tools/test/collect_cc_coverage.sh"
diff --git a/src/test/shell/bazel/bazel_coverage_test.sh b/src/test/shell/bazel/bazel_coverage_test.sh
index 07bc226..3eec2d1 100755
--- a/src/test/shell/bazel/bazel_coverage_test.sh
+++ b/src/test/shell/bazel/bazel_coverage_test.sh
@@ -21,13 +21,9 @@
 source "${CURRENT_DIR}/../integration_test_setup.sh" \
   || { echo "integration_test_setup.sh not found!" >&2; exit 1; }
 
-function test_cc_test_coverage() {
-  local -r LCOV=$(which lcov)
-  if [[ ! -x ${LCOV:-/usr/bin/lcov} ]]; then
-    echo "lcov not installed. Skipping test."
-    return
-  fi
-
+# Writes the C++ source files and a corresponding BUILD file for which to
+# collect code coverage. The sources are a.cc, a.h and t.cc.
+function setup_a_cc_lib_and_t_cc_test() {
   cat << EOF > BUILD
 cc_library(
     name = "a",
@@ -66,29 +62,148 @@
   a(true);
 }
 EOF
+}
+
+# Asserts if the given expected coverage result is included in the given output
+# file.
+#
+# - expected_coverage The expected result that must be included in the output.
+# - output_file       The location of the coverage output file.
+function assert_coverage_result() {
+    local expected_coverage="${1}"; shift
+    local output_file="${1}"; shift
+
+    # Replace newlines with commas to facilitate the assertion.
+    local expected_coverage_no_newlines="$( echo "$expected_coverage" | tr '\n' ',' )"
+    local output_file_no_newlines="$( cat "$output_file" | tr '\n' ',' )"
+
+    ( echo $output_file_no_newlines | grep  $expected_coverage_no_newlines ) \
+        || fail "Expected coverage result
+<$expected_coverage>
+was not found in actual coverage report:
+<$( cat "$output_file" )>"
+}
+
+# Returns the path of the code coverage report that was generated by Bazel by
+# looking at the current $TEST_log. The method fails if TEST_log does not
+# contain any coverage report for a passed test.
+function get_coverage_file_path_from_test_log() {
+  local ending_part="$(sed -n -e '/PASSED/,$p' "$TEST_log")"
+
+  local coverage_file_path=$(grep -Eo "/[/a-zA-Z0-9\.\_\-]+\.dat$" <<< "$ending_part")
+  [[ -e "$coverage_file_path" ]] || fail "Coverage output file does not exist!"
+  echo "$coverage_file_path"
+}
+
+function test_cc_test_coverage_lcov() {
+  local -r lcov_location=$(which lcov)
+  if [[ ! -x "${lcov_location:-/usr/bin/lcov}" ]]; then
+    echo "lcov not installed. Skipping test."
+    return
+  fi
+
+  setup_a_cc_lib_and_t_cc_test
 
   bazel coverage --test_output=all --build_event_text_file=bep.txt //:t \
-      &>$TEST_log || fail "Coverage for //:t failed"
+      &>"$TEST_log" || fail "Coverage for //:t failed"
 
-  ending_part=$(sed -n -e '/PASSED/,$p' $TEST_log)
+  local coverage_output_file="$( get_coverage_file_path_from_test_log )"
 
-  coverage_file_path=$(grep -Eo "/[/a-zA-Z0-9\.\_\-]+\.dat$" <<< "$ending_part")
-  [ -e $coverage_file_path ] || fail "Coverage output file does not exist!"
+  # Check the expected coverage for a.cc in the coverage file.
+  local expected_result_a_cc="SF:a.cc
+FN:3,_Z1ab
+FNDA:1,_Z1ab
+FNF:1
+FNH:1
+DA:3,1
+DA:4,1
+DA:5,1
+DA:7,0
+LH:3
+LF:4
+end_of_record"
+  assert_coverage_result "$expected_result_a_cc" "$coverage_output_file"
 
-  # Check if a.cc is in the coverage file
-  assert_contains "^SF:.*a.cc$" "$coverage_file_path"
-  # Check if the only branch in a() has correct coverage:
-  assert_contains "^DA:5,1$" "$coverage_file_path"  # true branch should be taken
-  assert_contains "^DA:7,0$" "$coverage_file_path"  # false branch should not be
 
-  # Verify the files are reported correctly in the build event protocol.
-  assert_contains 'name: "test.lcov"' bep.txt
-  assert_contains 'name: "baseline.lcov"' bep.txt
+  # Check the expected coverage for t.cc in the coverage file.
+  local expected_result_t_cc="SF:t.cc
+FN:4,main
+FNDA:1,main
+FNF:1
+FNH:1
+DA:4,1
+DA:5,1
+DA:6,1
+LH:3
+LF:3
+end_of_record"
+  assert_coverage_result "$expected_result_t_cc" "$coverage_output_file"
 
   # Verify that this is also true for cached coverage actions.
   bazel coverage --test_output=all --build_event_text_file=bep.txt //:t \
-      &>$TEST_log || fail "Coverage for //:t failed"
+      &>"$TEST_log" || fail "Coverage for //:t failed"
   expect_log '//:t.*cached'
+  # Verify the files are reported correctly in the build event protocol.
+  assert_contains 'name: "test.lcov"' bep.txt
+  assert_contains 'name: "baseline.lcov"' bep.txt
+}
+
+function test_cc_test_coverage_gcov() {
+  local -r gcov_location=$(which gcov)
+  if [[ ! -x ${gcov_location:-/usr/bin/gcov} ]]; then
+    echo "gcov not installed. Skipping test."
+    return
+  fi
+
+  "$gcov_location" -version | grep "LLVM" && \
+      echo "gcov LLVM version not supported. Skipping test." && return
+
+  setup_a_cc_lib_and_t_cc_test
+
+  bazel coverage --experimental_cc_coverage --test_output=all \
+     --build_event_text_file=bep.txt //:t &>"$TEST_log" \
+     || fail "Coverage for //:t failed"
+
+  local coverage_file_path="$( get_coverage_file_path_from_test_log )"
+
+  # Check the expected coverage for a.cc in the coverage file.
+  local expected_result_a_cc="SF:a.cc
+FN:3,_Z1ab
+FNDA:1,_Z1ab
+FNF:1
+FNH:1
+BA:4,2
+BRF:1
+BRH:1
+DA:3,1
+DA:4,1
+DA:5,1
+DA:7,0
+LH:3
+LF:4
+end_of_record"
+  assert_coverage_result "$expected_result_a_cc" "$coverage_file_path"
+
+  # Check the expected coverage for t.cc in the coverage file.
+  local expected_result_t_cc="SF:t.cc
+FN:4,main
+FNDA:1,main
+FNF:1
+FNH:1
+DA:4,1
+DA:5,1
+DA:6,1
+LH:3
+LF:3
+end_of_record"
+  assert_coverage_result "$expected_result_t_cc" "$coverage_file_path"
+
+  # Verify that this is also true for cached coverage actions.
+  bazel coverage --experimental_cc_coverage --test_output=all \
+      --build_event_text_file=bep.txt //:t \
+      &>"$TEST_log" || fail "Coverage for //:t failed"
+  expect_log '//:t.*cached'
+  # Verify the files are reported correctly in the build event protocol.
   assert_contains 'name: "test.lcov"' bep.txt
   assert_contains 'name: "baseline.lcov"' bep.txt
 }
@@ -200,10 +315,7 @@
 
   bazel coverage --test_output=all //:test &>$TEST_log || fail "Coverage for //:test failed"
   cat $TEST_log
-  ending_part=$(sed -n -e '/PASSED/,$p' $TEST_log)
-
-  coverage_file_path=$(grep -Eo "/[/a-zA-Z0-9\.\_\-]+\.dat$" <<< "$ending_part")
-  [ -e $coverage_file_path ] || fail "Coverage output file does not exist!"
+  local coverage_file_path="$( get_coverage_file_path_from_test_log )"
 
   cat <<EOF > result.dat
 SF:com/example/Collatz.java
@@ -384,10 +496,7 @@
 EOF
 
   bazel coverage --test_output=all --experimental_java_coverage //:test &>$TEST_log || fail "Coverage for //:test failed"
-  ending_part=$(sed -n -e '/PASSED/,$p' $TEST_log)
-
-  coverage_file_path=$(grep -Eo "/[/a-zA-Z0-9\.\_\-]+\.dat$" <<< "$ending_part")
-  [ -e $coverage_file_path ] || fail "Coverage output file not exists!"
+  local coverage_file_path="$( get_coverage_file_path_from_test_log )"
 
   cat <<EOF > result.dat
 SF:src/main/com/example/Collatz.java
@@ -472,10 +581,7 @@
 
   bazel coverage --test_output=all //:orange-sh &>$TEST_log || fail "Coverage for //:orange-sh failed"
 
-  ending_part=$(sed -n -e '/PASSED/,$p' $TEST_log)
-
-  coverage_file_path=$(grep -Eo "/[/a-zA-Z0-9\.\_\-]+\.dat$" <<< "$ending_part")
-  [ -e $coverage_file_path ] || fail "Coverage output file not exists!"
+  local coverage_file_path="$( get_coverage_file_path_from_test_log )"
 
   cat <<EOF > result.dat
 SF:com/google/orange/orangeBin.java
diff --git a/tools/test/collect_cc_coverage.sh b/tools/test/collect_cc_coverage.sh
index c0d6b36..87a74a6 100755
--- a/tools/test/collect_cc_coverage.sh
+++ b/tools/test/collect_cc_coverage.sh
@@ -27,7 +27,6 @@
 # - COVERAGE_DIR            Directory containing metadata files needed for
 #                           coverage collection (e.g. gcda files, profraw).
 # - COVERAGE_MANIFEST       Location of the instrumented file manifest.
-# - COVERAGE_OUTPUT_FILE    Location of the final coverage report.
 # - COVERAGE_GCOV_PATH      Location of gcov. This is set by the TestRunner.
 # - ROOT                    Location from where the code coverage collection
 #                           was invoked.
@@ -44,6 +43,12 @@
   return 1
 }
 
+# Returns 0 if gcov must be used, 1 otherwise.
+function uses_gcov() {
+  [[ "$GCOV_COVERAGE" -eq "1"  ]] && return 0
+  return 1
+}
+
 function init_gcov() {
   # Symlink the gcov tool such with a link called gcov. Clang comes with a tool
   # called llvm-cov, which behaves like gcov if symlinked in this way (otherwise
@@ -53,48 +58,183 @@
   ln -s "${COVERAGE_GCOV_PATH}" "${GCOV}"
 }
 
-# Computes code coverage data using the clang generated metadata found under $COVERAGE_DIR.
-# Writes the collected coverage into ${COVERAGE_OUTPUT_FILE}.
+# Computes code coverage data using the clang generated metadata found under
+# $COVERAGE_DIR.
+# Writes the collected coverage into the given output file.
 function llvm_coverage() {
+  local output_file="${1}"; shift
   export LLVM_PROFILE_FILE="${COVERAGE_DIR}/%h-%p-%m.profraw"
-  "${COVERAGE_GCOV_PATH}" merge -output "${COVERAGE_OUTPUT_FILE}" "${COVERAGE_DIR}"/*.profraw
+  "${COVERAGE_GCOV_PATH}" merge -output "${output_file}" \
+      "${COVERAGE_DIR}"/*.profraw
 }
 
 # Computes code coverage data using gcda files found under $COVERAGE_DIR.
-# Writes the collected coverage into ${COVERAGE_OUTPUT_FILE} in lcov format.
+# Writes the collected coverage into the given output file in lcov format.
 function lcov_coverage() {
+  local output_file="${1}"; shift
+
   cat "${COVERAGE_MANIFEST}" | grep ".gcno$" | while read gcno; do
     mkdir -p "${COVERAGE_DIR}/$(dirname ${gcno})"
     cp "${ROOT}/${gcno}" "${COVERAGE_DIR}/${gcno}"
   done
+
+  local lcov_tool="$(which lcov)"
+  if [[ ! -x "$lcov_tool" ]]; then
+    lcov_tool=/usr/bin/lcov
+  fi
+
   # Run lcov over the .gcno and .gcda files to generate the lcov tracefile.
   # -c                    - Collect coverage data
   # --no-external         - Do not collect coverage data for system files
-  # --ignore-errors graph - Ignore missing .gcno files; Bazel only instruments some files
+  # --ignore-errors graph - Ignore missing .gcno files; Bazel only instruments
+  #                         some files
   # -q                    - Quiet mode
   # --gcov-tool "${GCOV}" - Pass the local symlink to be uses as gcov by lcov
   # -b /proc/self/cwd     - Use this as a prefix for all source files instead of
   #                         the current directory
   # -d "${COVERAGE_DIR}"  - Directory to search for .gcda files
   # -o "${COVERAGE_OUTPUT_FILE}" - Output file
-  LCOV=$(which lcov)
-  if [[ ! -x $LCOV ]]; then
-    LCOV=/usr/bin/lcov
-  fi
-  $LCOV -c --no-external --ignore-errors graph -q \
+  $lcov_tool -c --no-external --ignore-errors graph \
       --gcov-tool "${GCOV}" -b /proc/self/cwd \
-      -d "${COVERAGE_DIR}" -o "${COVERAGE_OUTPUT_FILE}"
-   # Fix up the paths to be relative by removing the prefix we specified above.
-  sed -i -e "s*/proc/self/cwd/**g" "${COVERAGE_OUTPUT_FILE}"
+      -d "${COVERAGE_DIR}" -o "${output_file}"
+
+  # Fix up the paths to be relative by removing the prefix we specified above.
+  sed -i -e "s*/proc/self/cwd/**g" "${output_file}"
+}
+
+# Generates a code coverage report in gcov intermediate text format by invoking
+# gcov and using the profile data (.gcda) and notes (.gcno) files.
+#
+# The profile data files are expected to be found under $COVERAGE_DIR.
+# The notes file are expected to be found under $ROOT.
+#
+# - output_file     The location of the file where the generated code coverage
+#                   report is written.
+function gcov_coverage() {
+  local output_file="${1}"; shift
+
+  touch "$output_file"
+
+  # Move .gcno files in $COVERAGE_DIR as the gcda files, because gcov
+  # expects them to be under the same directory.
+  cat "${COVERAGE_MANIFEST}" | grep ".gcno$" | while read gcno_path; do
+
+    local gcda="${COVERAGE_DIR}/$(dirname ${gcno_path})/$(basename ${gcno_path} .gcno).gcda"
+    # If the gcda file was not found we generate empty coverage from the gcno
+    # file.
+    if [[ -f "$gcda" ]]; then
+        # gcov expects both gcno and gcda files to be in the same directory.
+        # We overcome this by copying the gcno to $COVERAGE_DIR where the gcda
+        # files are expected to be.
+        if [ ! -f "${COVERAGE_DIR}/${gcno_path}" ]; then
+            mkdir -p "${COVERAGE_DIR}/$(dirname ${gcno_path})"
+            cp "$ROOT/${gcno_path}" "${COVERAGE_DIR}/${gcno_path}"
+        fi
+        # Invoke gcov to generate a code coverage report with the flags:
+        # -i              Output gcov file in an intermediate text format.
+        #                 The output is a single .gcov file per .gcda file.
+        #                 No source code is required.
+        # -b              Write branch frequencies to the output file, and
+        #                 write branch summary info to the standard output.
+        # -o directory    The directory containing the .gcno and
+        #                 .gcda data files.
+        # "${gcda"}       The input file name. gcov is looking for data files
+        #                 named after the input filename without its extension.
+        "${GCOV}" -i -b -o "$(dirname ${gcda})" "${gcda}"
+
+        # gcov produces files called <source file name>.gcov in the current
+        # directory. These contain the coverage information of the source file
+        # they correspond to. One .gcov file is produced for each source
+        # (and/or header) file containing code which was compiled to produce
+        # the .gcda files.
+        # We try to find the correct source and header files that were generated
+        # for the current gcno.
+        # Retrieving every .gcov file that was generated in the current
+        # directory is not correct because it can contain coverage information
+        # for sources that are not included by the command line flag
+        # --instrumentation_filter.
+
+        local gcov_file="$(get_source_or_header_file source $gcno_path)"
+        if [ -f "$gcov_file" ]; then
+            cat "$gcov_file" >> "${output_file}"
+            # We don't need this file anymore.
+            rm -f "$gcov_file"
+        fi
+
+        gcov_file="$(get_source_or_header_file header $gcno_path)"
+        if [ -f "$gcov_file" ]; then
+            cat "$gcov_file" >> "${output_file}"
+            # We don't need this file anymore.
+            rm -f "$gcov_file"
+        fi
+    fi
+  done
+  echo "Coverage output file contains:"
+  cat "${output_file}"
+}
+
+# Returns a .gcov corresponding to either a C++ source file or a C++ header
+# file depending on the given file type, that could have been generated by gcov
+# for the given gcno file.
+#
+# - filetype     Can be either "source" or "header".
+# - gcno_file    The .gcno filename.
+function get_source_or_header_file() {
+    local filetype="${1}"; shift
+    local gcno_file="${1}"; shift
+
+    # gcov places results in the current working dir. The gcov documentation
+    # doesn't provide much details about how the name of the output file is
+    # generated, other than hinting at it being named  <source file name>.gcov.
+    # Since we only know the gcno filename, we try and see which of the
+    # following extensions the source file had.
+    declare -a source_extensions
+
+    case "$filetype" in
+      ("source") source_extensions=("" ".cc" ".cpp" ".c") ;;
+      ("header") source_extensions=(".h" ".hh") ;;
+    esac
+
+    declare -a is_pic_extensions=("" ".pic")
+
+    local gcov_file=""
+    for ext in "${source_extensions[@]}"
+    do
+      for pic_ext in "${is_pic_extensions[@]}"
+      do
+        gcov_file="$(basename ${gcno_file} "$pic_ext.gcno")$ext.gcov"
+        if [ -f "$gcov_file" ]; then
+          echo "$gcov_file" && return
+        fi
+      done
+    done
 }
 
 function main() {
   init_gcov
+
+  # If llvm code coverage is used, we output the raw code coverage report in
+  # the $COVERAGE_OUTPUT_FILE. This report will not be converted to any other
+  # format by LcovMerger.
+  # TODO(#5881): Convert profdata reports to lcov.
   if uses_llvm; then
-    llvm_coverage
-  else
-    lcov_coverage
+    llvm_coverage "$COVERAGE_OUTPUT_FILE" && exit 0
   fi
+
+  # When using either gcov or lcov, have an output file specific to the test
+  # and format used. For lcov we generate a ".dat" output file and for gcov
+  # a ".gcov" output file. It is important that these files are generated under
+  # COVERAGE_DIR.
+  # When this script is invoked by tools/test/collect_coverage.sh either of
+  # these two coverage reports will be picked up by LcovMerger and their
+  # content will be converted and/or merged with other reports to an lcov
+  # format, generating the final code coverage report.
+  case "$BAZEL_CC_COVERAGE_TOOL" in
+        ("GCOV") gcov_coverage "$COVERAGE_DIR/_cc_coverage.gcov" ;;
+        ("LCOV") lcov_coverage "$COVERAGE_DIR/_cc_coverage.dat" ;;
+        (*) echo "Coverage tool $BAZEL_CC_COVERAGE_TOOL not supported" \
+            && exit 1
+  esac
 }
 
-main
\ No newline at end of file
+main
diff --git a/tools/test/collect_coverage.sh b/tools/test/collect_coverage.sh
index b40ec9a..c05e3fd 100755
--- a/tools/test/collect_coverage.sh
+++ b/tools/test/collect_coverage.sh
@@ -74,22 +74,18 @@
 export BULK_COVERAGE_RUN=1
 
 
-# Only check if file exists when LCOV_MERGER is set
-if [[ ! "$COVERAGE_LEGACY_MODE" ]]; then
-  for name in "$LCOV_MERGER"; do
-    if [[ ! -e $name ]]; then
-      echo --
-      echo Coverage runner: cannot locate file $name
-      exit 1
-    fi
-  done
-fi
+for name in "$LCOV_MERGER"; do
+  if [[ ! -e $name ]]; then
+    echo --
+    echo Coverage runner: cannot locate file $name
+    exit 1
+  fi
+done
 
-if [[ "$COVERAGE_LEGACY_MODE" ]]; then
-  export GCOV_PREFIX_STRIP=3
-  export GCOV_PREFIX="${COVERAGE_DIR}"
-  export LLVM_PROFILE_FILE="${COVERAGE_DIR}/%h-%p-%m.profraw"
-fi
+# Setting up the environment for executing the C++ tests.
+export GCOV_PREFIX_STRIP=3
+export GCOV_PREFIX="${COVERAGE_DIR}"
+export LLVM_PROFILE_FILE="${COVERAGE_DIR}/%h-%p-%m.profraw"
 
 # TODO(iirina): cd should be avoided.
 cd "$TEST_SRCDIR/$TEST_WORKSPACE"
@@ -113,11 +109,25 @@
 
 if [[ "$CC_CODE_COVERAGE_SCRIPT" ]]; then
     eval "${CC_CODE_COVERAGE_SCRIPT}"
-    exit $TEST_STATUS
 fi
 
+# Export the command line that invokes LcovMerger with the flags:
+# --coverage_dir      The absolute path of the directory where the intermediate
+#                     coverage reports are located. LcovMerger will search for
+#                     files with the .dat and .gcov extension under this
+#                     directory
+#                     and will merge everything it found in the output report.
+# --output_file       The absolute path of the merged coverage report.
+# --filter_sources    Filters out the sources that match the given regexes
+#                     from the final coverage report. This is needed because
+#                     some coverage tools (e.g. gcov) do not have any way of
+#                     specifying what sources to exclude when generating the
+#                     code coverage report (in this case the syslib sources).
 export LCOV_MERGER_CMD="${LCOV_MERGER} --coverage_dir=${COVERAGE_DIR} \
---output_file=${COVERAGE_OUTPUT_FILE}"
+  --output_file=${COVERAGE_OUTPUT_FILE} \
+  --filter_sources=/usr/bin/.+ \
+  --filter_sources=/usr/lib/.+ \
+  --filter_sources=.*external/.+"
 
 
 if [[ $DISPLAY_LCOV_CMD ]] ; then