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