Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

coverage tool support #107

Merged
merged 10 commits into from
Sep 30, 2022
18 changes: 18 additions & 0 deletions .bazelrc
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,22 @@ build --copt="-Wall"
build --copt="-Werror"
build:windows --copt=/wd4716

# Coverage options
coverage --config=coverage
coverage --build_tests_only
build:coverage --action_env=BAZEL_USE_LLVM_NATIVE_COVERAGE=1
build:coverage --action_env=GCOV=llvm-profdata
build:coverage --combined_report=lcov
build:coverage --experimental_use_llvm_covmap
build:coverage --experimental_generate_llvm_lcov
build:coverage --collect_code_coverage
build:coverage --instrumentation_filter="//source[/:],//cpp2sky[/:]"
build:coverage --coverage_support=@cpp2sky//bazel/coverage:coverage_support
build:coverage --test_env=CC_CODE_COVERAGE_SCRIPT=external/cpp2sky/bazel/coverage/collect_cc_coverage.sh
build:coverage --strategy=TestRunner=local
build:coverage --strategy=CoverageReport=local
build:coverage --experimental_use_llvm_covmap
build:coverage --collect_code_coverage
build:coverage --test_tag_filters=-nocoverage

try-import %workspace%/user.bazelrc
2 changes: 1 addition & 1 deletion .bazelversion
Original file line number Diff line number Diff line change
@@ -1 +1 @@
3.7.2
4.0.0
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,6 @@ user.bazelrc
.vscode/

.clangd/
compile_commands.json
compile_commands.json

coverage_report
6 changes: 6 additions & 0 deletions bazel/coverage/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
licenses(["notice"]) # Apache 2

filegroup(
name = "coverage_support",
srcs = ["collect_cc_coverage.sh"],
)
208 changes: 208 additions & 0 deletions bazel/coverage/collect_cc_coverage.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
#!/bin/bash -x
# This is fork from https://raw.githubusercontent.com/bazelbuild/bazel/master/tools/test/collect_cc_coverage.sh

# 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.

# This script collects code coverage data for C++ sources, after the tests
# were executed.
#
# Bazel C++ code coverage collection support is poor and limited. There is
# an ongoing effort to improve this (tracking issue #1118).
#
# Bazel uses the lcov tool for gathering coverage data. There is also
# an experimental support for clang llvm coverage, which uses the .profraw
# data files to compute the coverage report.
#
# This script assumes the following environment variables are set:
# - COVERAGE_DIR Directory containing metadata files needed for
# coverage collection (e.g. gcda files, profraw).
# - COVERAGE_MANIFEST Location of the instrumented file manifest.
# - COVERAGE_GCOV_PATH Location of gcov. This is set by the TestRunner.
# - COVERAGE_GCOV_OPTIONS Additional options to pass to gcov.
# - ROOT Location from where the code coverage collection
# was invoked.
#
# The script looks in $COVERAGE_DIR for the C++ metadata coverage files (either
# gcda or profraw) and uses either lcov or gcov to get the coverage data.
# The coverage data is placed in $COVERAGE_OUTPUT_FILE.

# Checks if clang llvm coverage should be used instead of lcov.
function uses_llvm() {
if stat "${COVERAGE_DIR}"/*.profraw >/dev/null 2>&1; then
return 0
fi
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
# we would need to invoke it with "llvm-cov gcov").
# For more details see https://llvm.org/docs/CommandGuide/llvm-cov.html.
GCOV="${COVERAGE_DIR}/gcov"
if [ ! -f "${COVERAGE_GCOV_PATH}" ]; then
echo "GCov does not exist at the given path: '${COVERAGE_GCOV_PATH}'"
exit 1
fi
# When using a tool from a toolchain COVERAGE_GCOV_PATH will be a relative
# path. To make it work on different working directories it's required to
# convert the path to an absolute one.
COVERAGE_GCOV_PATH_ABS="$(cd "${COVERAGE_GCOV_PATH%/*}" && pwd)/${COVERAGE_GCOV_PATH##*/}"
ln -s "${COVERAGE_GCOV_PATH_ABS}" "${GCOV}"
}

# 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_lcov() {
local output_file="${1}"; shift
export LLVM_PROFILE_FILE="${COVERAGE_DIR}/%h-%p-%m.profraw"
"${COVERAGE_GCOV_PATH}" merge -output "${output_file}.data" \
"${COVERAGE_DIR}"/*.profraw

local object_param=""
while read -r line; do
if [[ ${line: -24} == "runtime_objects_list.txt" ]]; then
while read -r line_runtime_object; do
if [[ ${line_runtime_object} == *"absl"* ]]; then
continue
fi
object_param+=" -object ${RUNFILES_DIR}/${TEST_WORKSPACE}/${line_runtime_object}"
done < "${line}"
fi
done < "${COVERAGE_MANIFEST}"

"${LLVM_COV}" export -instr-profile "${output_file}.data" -format=lcov \
-ignore-filename-regex='.*external/.+' \
-ignore-filename-regex='/tmp/.+' \
${object_param} | sed 's#/proc/self/cwd/##' > "${output_file}"
}

function llvm_coverage_profdata() {
local output_file="${1}"; shift
export LLVM_PROFILE_FILE="${COVERAGE_DIR}/%h-%p-%m.profraw"
"${COVERAGE_GCOV_PATH}" merge -output "${output_file}" \
"${COVERAGE_DIR}"/*.profraw
}

# 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

# We'll save the standard output of each the gcov command in this log.
local gcov_log="$output_file.gcov.log"

# Copy .gcno files next to their corresponding .gcda files in $COVERAGE_DIR
# because gcov expects them to be in the same directory.
while read -r line; do
if [[ ${line: -4} == "gcno" ]]; then
gcno_path=${line}
local gcda="${COVERAGE_DIR}/$(dirname ${gcno_path})/$(basename ${gcno_path} .gcno).gcda"
# If the gcda file was not found we skip generating 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.
# -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 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
# (or header) file containing code which was compiled to produce the
# .gcda files.
# Don't generate branch coverage (-b) because of a gcov issue that
# segfaults when both -i and -b are used (see
# https://gcc.gnu.org/bugzilla/show_bug.cgi?id=84879).
"${GCOV}" -i $COVERAGE_GCOV_OPTIONS -o "$(dirname ${gcda})" "${gcda}"

# Extract gcov's version: the output of `gcov --version` contains the
# version as a set of major-minor-patch numbers, of which we extract
# the major version.
gcov_major_version=$("${GCOV}" --version | sed -n -E -e 's/^.*\s([0-9]+)\.[0-9]+\.[0-9]+\s?.*$/\1/p')

# Check the gcov version so we can process the data correctly
if [[ $gcov_major_version -ge 9 ]]; then
# gcov 9 or higher use a JSON based format for their coverage reports.
# The output is generated into multiple files: "$(basename ${gcda}).gcov.json.gz"
# Concatenating JSON documents does not yield a valid document, so they are moved individually
mv -- *.gcov.json.gz "$(dirname "$output_file")"
else
# Append all .gcov files in the current directory to the output file.
cat -- *.gcov >> "$output_file"
# Delete the .gcov files.
rm -- *.gcov
fi
fi
fi
done < "${COVERAGE_MANIFEST}"
}

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
if [[ "${GENERATE_LLVM_LCOV}" == "1" ]]; then
BAZEL_CC_COVERAGE_TOOL="LLVM_LCOV"
else
BAZEL_CC_COVERAGE_TOOL="PROFDATA"
fi
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" ;;
("PROFDATA") llvm_coverage_profdata "$COVERAGE_DIR/_cc_coverage.profdata" ;;
("LLVM_LCOV") llvm_coverage_lcov "$COVERAGE_DIR/_cc_coverage.dat" ;;
(*) echo "Coverage tool $BAZEL_CC_COVERAGE_TOOL not supported" \
&& exit 1
esac
}

main
75 changes: 75 additions & 0 deletions coverage.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
#!/bin/bash

set -e
#set -x

[[ -z "${SRCDIR}" ]] && SRCDIR="${PWD}"

OUTPUT_DIR="./coverage_report/"
DATA_DIR="${SRCDIR}/bazel-testlogs/"
PROJECT=`basename "${SRCDIR}"`

# This is the target that will be run to generate coverage data. It can be overridden by consumer
# projects that want to run coverage on a different/combined target.
# Command-line arguments take precedence over ${COVERAGE_TARGET}.
if [[ $# -gt 0 ]]; then
COVERAGE_TARGETS=$*
elif [[ -n "${COVERAGE_TARGET}" ]]; then
COVERAGE_TARGETS=${COVERAGE_TARGET}
else
COVERAGE_TARGETS=//test/...
fi


echo "Starting gen_coverage.sh..."
echo " PWD=$(pwd)"
echo " OUTPUT_DIR=${OUTPUT_DIR}"
echo " DATA_DIR=${DATA_DIR}"
echo " TARGETS=${COVERAGE_TARGETS}"

function gen_coverage_data() {
echo "Generating coverage data..."
bazel coverage "$1" --test_output=errors
}

function purge_files() {
local src_files="$1"
local purge_files="$2"

for file in ${src_files}
do
if [ -s "${file}" ] ; then
mv "${file}" "${file}".bak
lcov --remove "${file}".bak "${purge_files}" -o "${file}"
chmod 777 "${file}"
fi
done
}

function gen_report() {
echo "Generating report..."

local output_directory="$1"

# The location where coverage.dat files were stored
path="${DATA_DIR}"

datfiles=$(find ${path} -name "coverage.dat")

purge_files ${datfiles} "pb.h pb.cc"

find ${path} -name "coverage.dat" -size 0 -delete

datfiles=$(find ${path} -name "coverage.dat")

# add --branch-coverage to show branch coverage
genhtml --title ${PROJECT} --ignore-errors "source" ${datfiles} -o "${OUTPUT_DIR}"
tar -zcf ${PROJECT}_coverage.tar.gz ${OUTPUT_DIR}
mv ${PROJECT}_coverage.tar.gz ${OUTPUT_DIR}

echo "HTML coverage report is in ${OUTPUT_DIR}/index.html"
echo "All coverage report files are in ${OUTPUT_DIR}/${PROJECT}_coverage.tar.gz"
}

gen_coverage_data ${COVERAGE_TARGETS}
gen_report ${OUTPUT_DIR}