Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions .bazelci/presubmit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,26 @@
# https://github.com/bazelbuild/rules_cc/issues/613
? "--test_env=IGNORE_COVERAGE_COLLECTION_FAILURES=1"

# Assert that `bazel coverage` actually collected something; see
# .bazelci/verify_coverage.sh for why the check is a named list of sources
# rather than a per-test assertion. Only these two qualify: they are
# instrumented under the default `--instrumentation_filter` on every supported
# Bazel version, whereas everything else that reports coverage lives under
# `//tests/builtins_bzl/...`, which is excluded before Bazel 9.
.verify_coverage: &verify_coverage
- >-
./.bazelci/verify_coverage.sh
--expect=tests/alwayslink_srcs/registered.c
--expect=tests/local_includes/lib/lib.c

# Coverage collection on Windows is already known to be unreliable -- see the
# IGNORE_COVERAGE_COLLECTION_FAILURES escape hatch above and
# https://github.com/bazelbuild/rules_cc/issues/613. Rather than assert on
# something the platform may not deliver, the Windows jobs only report what was
# collected so the tally is visible in the log.
.report_coverage: &report_coverage
- "./.bazelci/verify_coverage.sh --report-only"

.rule_based_toolchain_config: &rule_based_toolchain_config
working_directory: examples/rule_based_toolchain
build_flags:
Expand Down Expand Up @@ -81,6 +101,7 @@ tasks:
test_targets: *test_targets
coverage_flags: *common_coverage_flags
coverage_targets: *coverage_targets
post_shell_commands: *verify_coverage
macos_intel_head:
name: MacOS Intel (Bazel HEAD)
bazel: last_green
Expand All @@ -93,6 +114,7 @@ tasks:
test_flags: *macos_test_flags
coverage_flags: *common_coverage_flags
coverage_targets: *coverage_targets
post_shell_commands: *verify_coverage
macos_head:
name: MacOS (Bazel HEAD)
bazel: last_green
Expand All @@ -102,6 +124,7 @@ tasks:
test_flags: *macos_test_flags
coverage_flags: *common_coverage_flags
coverage_targets: *coverage_targets
post_shell_commands: *verify_coverage
windows_head:
name: Windows (Bazel HEAD)
bazel: last_green
Expand All @@ -111,6 +134,7 @@ tasks:
test_flags: *windows_test_flags
coverage_flags: *windows_coverage_flags
coverage_targets: *coverage_targets
post_shell_commands: *report_coverage

# Bazel@rolling
ubuntu2004_rolling:
Expand All @@ -121,6 +145,7 @@ tasks:
test_targets: *test_targets
coverage_flags: *common_coverage_flags
coverage_targets: *coverage_targets
post_shell_commands: *verify_coverage
macos_intel_rolling:
name: MacOS Intel (Bazel rolling)
bazel: rolling
Expand All @@ -133,6 +158,7 @@ tasks:
test_flags: *macos_test_flags
coverage_flags: *common_coverage_flags
coverage_targets: *coverage_targets
post_shell_commands: *verify_coverage
macos_rolling:
name: MacOS (Bazel rolling)
bazel: rolling
Expand All @@ -142,6 +168,7 @@ tasks:
test_flags: *macos_test_flags
coverage_flags: *common_coverage_flags
coverage_targets: *coverage_targets
post_shell_commands: *verify_coverage
windows_rolling:
name: Windows (Bazel rolling)
bazel: rolling
Expand All @@ -162,6 +189,7 @@ tasks:
test_targets: *test_targets_bazel_less_than_9
coverage_flags: *common_coverage_flags
coverage_targets: *coverage_targets_bazel_less_than_9
post_shell_commands: *verify_coverage
macos_intel_bazel_7:
name: MacOS Intel (Bazel 7)
bazel: 7.x
Expand All @@ -173,6 +201,7 @@ tasks:
test_flags: *macos_test_flags
coverage_flags: *common_coverage_flags
coverage_targets: *coverage_targets_bazel_less_than_9
post_shell_commands: *verify_coverage
macos_bazel_7:
name: MacOS (Bazel 7)
bazel: 7.x
Expand All @@ -184,6 +213,7 @@ tasks:
test_flags: *macos_test_flags
coverage_flags: *common_coverage_flags
coverage_targets: *coverage_targets_bazel_less_than_9
post_shell_commands: *verify_coverage
windows_bazel_7:
name: Windows (Bazel 7)
bazel: 7.x
Expand All @@ -203,6 +233,7 @@ tasks:
test_targets: *test_targets_bazel_less_than_9
coverage_flags: *common_coverage_flags
coverage_targets: *coverage_targets_bazel_less_than_9
post_shell_commands: *verify_coverage
macos_intel_bazel_8:
name: MacOS Intel (Bazel 8)
bazel: 8.x
Expand All @@ -212,6 +243,7 @@ tasks:
test_flags: *macos_test_flags
coverage_flags: *common_coverage_flags
coverage_targets: *coverage_targets_bazel_less_than_9
post_shell_commands: *verify_coverage
macos_bazel_8:
name: MacOS (Bazel 8)
bazel: 8.x
Expand All @@ -221,6 +253,7 @@ tasks:
test_flags: *macos_test_flags
coverage_flags: *common_coverage_flags
coverage_targets: *coverage_targets_bazel_less_than_9
post_shell_commands: *verify_coverage
windows_bazel_8:
name: Windows (Bazel 8)
bazel: 8.x
Expand All @@ -230,6 +263,7 @@ tasks:
test_flags: *windows_test_flags
coverage_flags: *windows_coverage_flags
coverage_targets: *test_targets_bazel_less_than_9
post_shell_commands: *report_coverage

# Bazel 9
ubuntu2004:
Expand All @@ -240,6 +274,7 @@ tasks:
test_targets: *test_targets
coverage_flags: *common_coverage_flags
coverage_targets: *coverage_targets
post_shell_commands: *verify_coverage
macos_intel:
name: MacOS Intel (Bazel 9)
bazel: 9.x
Expand All @@ -249,6 +284,7 @@ tasks:
test_flags: *macos_test_flags
coverage_flags: *common_coverage_flags
coverage_targets: *coverage_targets
post_shell_commands: *verify_coverage
macos:
name: MacOS (Bazel 9)
bazel: 9.x
Expand All @@ -258,6 +294,7 @@ tasks:
test_flags: *macos_test_flags
coverage_flags: *common_coverage_flags
coverage_targets: *coverage_targets
post_shell_commands: *verify_coverage
windows:
name: Windows (Bazel 9)
bazel: 9.x
Expand All @@ -266,6 +303,7 @@ tasks:
test_flags: *windows_test_flags
coverage_flags: *windows_coverage_flags
coverage_targets: *test_targets
post_shell_commands: *report_coverage

ubuntu_bzlmod:
name: Ubuntu 20.04 (Bazel 9, bzlmod)
Expand Down
134 changes: 134 additions & 0 deletions .bazelci/verify_coverage.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
#!/usr/bin/env bash
#
# Copyright 2026 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.
#
# Asserts that a preceding `bazel coverage` invocation actually collected line
# coverage.
#
# `bazel coverage` exits successfully even when every report it produces is
# empty, so a broken coverage collector looks exactly like a green build. Worse,
# a partially broken collector can emit well-formed LCOV that names source files
# but attributes no executed lines to them. This script therefore works per
# `SF:` block, pairing each source file with the `DA:` records inside its own
# block, and only counts a file as covered when at least one of those records
# has a non-zero hit count.
#
# Not every test yields coverage: `--instrumentation_filter` defaults to the
# package under test, so a test that only exercises sources in other packages
# legitimately produces an empty report. Files that are linked in but never
# entered legitimately report zero hits too. The check is consequently a floor
# plus an explicit list of source files that must be covered, rather than a
# per-test or per-file assertion.
#
# Usage:
# verify_coverage.sh [--report-only] [--expect=SOURCE_PATH]...
#
# --expect=PATH Require a covered source file whose `SF:` path contains
# PATH. May be repeated. Implies requiring at least one
# covered file overall, which is also the default.
# --report-only Print the tally and exit 0 without asserting anything.
# Any --expect is ignored. Used on platforms where coverage
# collection is known to be unreliable.

set -euo pipefail

report_only=false
expected=()

for arg in "$@"; do
case "${arg}" in
--report-only) report_only=true ;;
--expect=*) expected+=("${arg#--expect=}") ;;
*)
echo "ERROR: unrecognized argument '${arg}'" >&2
exit 1
;;
esac
done

testlogs="bazel-testlogs"
if [[ ! -d "${testlogs}" ]]; then
testlogs="$(bazel info bazel-testlogs 2>/dev/null || true)"
fi
if [[ -z "${testlogs}" || ! -d "${testlogs}" ]]; then
echo "ERROR: could not locate bazel-testlogs; was 'bazel coverage' run?" >&2
exit 1
fi

reports=()
populated=0
while IFS= read -r report; do
reports+=("${report}")
if [[ -s "${report}" ]]; then
populated=$((populated + 1))
fi
done < <(find -L "${testlogs}" -name coverage.dat -type f)

covered="$(mktemp)"
covered_paths="$(mktemp)"
trap 'rm -f "${covered}" "${covered_paths}"' EXIT

# One pass over every report, emitting "<executed lines>\t<source path>" for
# each source that ended up with a non-zero hit count. Accumulating per SF:
# block is what distinguishes "this file has coverage data" from "this file was
# merely listed in the report".
if [[ "${#reports[@]}" -gt 0 ]]; then
awk '
/^SF:/ { sf = substr($0, 4); next }
/^DA:/ {
split(substr($0, 4), record, ",")
if (sf != "" && record[2] + 0 > 0) { hits[sf]++ }
next
}
/^end_of_record/ { sf = ""; next }
END { for (s in hits) { print hits[s] "\t" s } }
' "${reports[@]}" | sort -k2 >"${covered}"
fi
cut -f2 "${covered}" >"${covered_paths}"

covered_files="$(wc -l <"${covered}" | tr -d '[:space:]')"
covered_lines="$(awk -F'\t' '{ total += $1 } END { print total + 0 }' "${covered}")"

echo "Coverage reports found: ${#reports[@]}"
echo "Reports with content: ${populated}"
echo "Source files with hit lines: ${covered_files}"
echo "Executed lines recorded: ${covered_lines}"
awk -F'\t' '{ printf " %6d hit lines %s\n", $1, $2 }' "${covered}"

if [[ "${report_only}" == "true" ]]; then
exit 0
fi

status=0

if [[ "${covered_files}" -eq 0 ]]; then
echo >&2
echo "ERROR: no source file came back with executed lines. Coverage" >&2
echo " collection is broken -- see //cc/private/coverage." >&2
status=1
fi

for want in ${expected[@]+"${expected[@]}"}; do
if ! grep -Fq -- "${want}" "${covered_paths}"; then
echo >&2
echo "ERROR: no executed lines recorded for a source file matching" >&2
echo " '${want}'. Either coverage collection regressed, or the test" >&2
echo " that exercises it no longer runs on this platform. The" >&2
echo " expected paths are listed in .bazelci/presubmit.yml." >&2
status=1
fi
done

exit "${status}"
45 changes: 44 additions & 1 deletion cc/private/coverage/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -1,15 +1,58 @@
load("@rules_cc//cc:cc_binary.bzl", "cc_binary")
load("@rules_cc//cc:cc_library.bzl", "cc_library")

config_setting(
name = "incompatible_use_native_collect_cc_coverage_enabled",
flag_values = {
"//cc/settings:incompatible_use_native_collect_cc_coverage": "true",
},
)

config_setting(
name = "experimental_use_cc_coverage_collector_enabled",
flag_values = {
"//cc/settings:experimental_use_cc_coverage_collector": "true",
},
)

# The coverage collector is compiled by whichever C++ toolchain the depending
# repository happens to configure, so the language standard is pinned here
# instead of being inherited from that repository's `--cxxopt` defaults. The
# sources target C++11, which every compiler released since ~2013 supports.
_STD_COPTS = select({
"//cc/compiler:clang-cl": [],
"//cc/compiler:msvc-cl": [],
"//conditions:default": ["-std=c++11"],
})

cc_library(
name = "coverage_utils",
srcs = ["coverage_utils.cc"],
hdrs = ["coverage_utils.h"],
copts = _STD_COPTS,
)

cc_binary(
name = "collect_coverage",
srcs = ["collect_coverage.cc"],
copts = _STD_COPTS,
visibility = ["//visibility:public"],
deps = [":coverage_utils"],
)

alias(
name = "collector",
actual = select({
":experimental_use_cc_coverage_collector_enabled": ":collect_coverage",
"//conditions:default": "collect_cc_coverage.sh",
}),
)

filegroup(
name = "collect_cc_coverage",
srcs = select({
":incompatible_use_native_collect_cc_coverage_enabled": ["@bazel_tools//tools/test:collect_cc_coverage"],
"//conditions:default": ["collect_cc_coverage.sh"],
"//conditions:default": [":collector"],
}),
visibility = ["//visibility:public"],
)
Expand Down
Loading