Skip to content
Merged
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
2 changes: 1 addition & 1 deletion MODULE.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,7 @@ llvm.toolchain(
# This is the agreed way to ensure linking for targets using std::atomic operations.
"-latomic",
]},
llvm_version = "19.1.7",
llvm_version = "22.1.7",
Comment thread
LittleHuba marked this conversation as resolved.
stdlib = {"": "stdc++"},
)
llvm.sysroot(
Expand Down
2 changes: 1 addition & 1 deletion quality/compiler_warnings/test/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ build_test(
src = "has_warnings/{}.cpp".format(case),
compile_stderr = matcher.contains_extended_regex(
# Convert e.g. -Wsign-compare to a flexible regex for stderr matching.
"Werror.*{}".format(
"error.*{}".format(
warning[0][2:].replace("-", ".*"),
),
),
Expand Down
11 changes: 11 additions & 0 deletions quality/coverage/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ load("@rules_cc//cc/toolchains:args.bzl", "cc_args")
load("@rules_cc//cc/toolchains:feature.bzl", "cc_feature")
load("@rules_python//python:defs.bzl", "py_binary")
load("@rules_shell//shell:sh_binary.bzl", "sh_binary")
load("//quality/coverage:coverage_scope.bzl", "coverage_scope")

sh_binary(
name = "generate_coverage_html",
Expand Down Expand Up @@ -64,3 +65,13 @@ cc_feature(
feature_name = "enable_llvm_coverage_for_death_tests",
visibility = ["//visibility:public"],
)

coverage_scope(
name = "coverage_scope",
testonly = True,
visibility = ["//quality/coverage:__subpackages__"],
deps = [
"//score/message_passing",
"//score/mw/com",
],
)
8 changes: 5 additions & 3 deletions quality/coverage/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ bazel run //quality/coverage:generate_coverage_html -- --platform qnx
```

For Linux (LLVM pipeline), the custom reporter produces a zip with an HTML report directly.
For QNX (gcov pipeline), the script converts the LCOV data to HTML using `lcov_to_html.py` (which internally uses gcovr).
For QNX (gcov pipeline), the script first resolves the collected coverage allowlist, then converts the LCOV data to HTML using `lcov_to_html.py` (which internally uses gcovr).

The report is written to `cpp_coverage/` (Linux) or `cpp_coverage_qnx/` (QNX). Open it:

Expand Down Expand Up @@ -145,7 +145,7 @@ bazel coverage //...
│ • Packages profdata + metadata into a zip
└── Final: reporter.py (--coverage_report_generator)
• Merges all per-test profdata into one
• Merges per-test profdata artifacts into a single merged profdata and uses `llvm-cov --empty-profile` for baseline-only archives.
• Runs llvm-cov show → HTML report
• Runs llvm-cov export → LCOV data
• Runs llvm-cov report → text summary
Expand Down Expand Up @@ -174,12 +174,14 @@ bazel run //quality/coverage:generate_coverage_html [-- --platform <linux|qnx>]
└── generate_coverage_html.sh
├── Detect format: zip (LLVM) or LCOV text (gcov)
├── LLVM: Extract HTML from zip → cpp_coverage_linux/
│ gcov: Convert LCOV → HTML via lcov_to_html.py (gcovr) → cpp_coverage_qnx/
│ gcov: Build collected allowlist + merge baseline_coverage.dat inputs → convert LCOV → HTML via lcov_to_html.py (gcovr) → cpp_coverage_qnx/
├── justify.py: YAML + code markers → manifest.json
├── effective_coverage.py: Post-process HTML + calculate effective %
└── Print summary + threshold check
```

For QNX, the allowlist from `quality/coverage:coverage_scope` is the authoritative file scope for the HTML report. Regex-based file filters are no longer used in that path.

## Configuration

### coverage.bazelrc
Expand Down
10 changes: 4 additions & 6 deletions quality/coverage/coverage.bazelrc
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
# ============================================================================
coverage --nocache_test_results
coverage --cxxopt=-O0
coverage --instrumentation_filter="^//score/message_passing[/:],^//score/mw/com/(impl|gateway|dependability|design|example|mocking|doc)"
coverage --combined_report=lcov
coverage --experimental_fetch_all_coverage_outputs

Expand All @@ -34,11 +33,10 @@ coverage --dynamic_mode=off
# Uses LLVM's source-based coverage instrumentation (--experimental_use_llvm_covmap)
# with custom merger/reporter that produce HTML reports via llvm-cov directly.
#
# NOTE: --experimental_use_llvm_covmap causes Bazel to instrument ALL targets
# regardless of --instrumentation_filter. The actual source filtering happens
# in the merger/reporter via --ignore-filename-regex. The instrumentation_filter
# is kept for documentation purposes and in case this Bazel limitation is fixed
# in the future.
# NOTE: --experimental_use_llvm_covmap causes Bazel to instrument ALL targets.
# Source filtering is handled at report time by the reporter using the coverage
# allowlist generated from dependable_element declarations (coverage_scope rule).
# See quality/coverage/coverage_scope.bzl for details.

coverage --experimental_generate_llvm_lcov
coverage --experimental_use_llvm_covmap
Expand Down
186 changes: 186 additions & 0 deletions quality/coverage/coverage_scope.bzl
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
# *******************************************************************************
# Copyright (c) 2026 Contributors to the Eclipse Foundation
#
# See the NOTICE file(s) distributed with this work for additional
# information regarding copyright ownership.
#
# This program and the accompanying materials are made available under the
# terms of the Apache License Version 2.0 which is available at
# https://www.apache.org/licenses/LICENSE-2.0
#
# SPDX-License-Identifier: Apache-2.0
# *******************************************************************************

"""
Coverage scope rule for deriving file-level allowlists from dependable_element structure.

This rule uses an aspect to traverse the build graph starting from
dependable_element _index targets, through component and unit targets, into
their implementation cc_library targets (and their transitive deps). At each
cc_library, it collects the actual source files (srcs + hdrs).

The resulting allowlist contains one source file path per line (relative to the
workspace root). The coverage reporter uses this to restrict reports to exactly
the files that are part of a dependable_element's implementation.
"""

# =============================================================================
# Provider to carry collected source file paths through the aspect
# =============================================================================

_CoverageScopeInfo = provider(
doc = "Carries source file paths, cc_library labels, and object files collected by the coverage scope aspect.",
fields = {
"source_files": "Depset of source file path strings (workspace-relative).",
"object_files": "Depset of compiled .o File objects for baseline coverage.",
},
)

# =============================================================================
# Aspect: traverses component hierarchy and cc_library deps to collect files
# =============================================================================

def _coverage_scope_aspect_impl(target, ctx):
"""Collects source file paths, cc_library labels, and archive files from the build graph."""
direct_files = []
direct_archives = []
transitive = []
transitive_archives = []

# At cc_library targets: collect srcs, hdrs, label, and static archive
if CcInfo in target:
for attr_name in ["srcs", "hdrs"]:
if hasattr(ctx.rule.attr, attr_name):
for src in getattr(ctx.rule.attr, attr_name):
for f in src.files.to_list():
if not f.path.startswith("external/") and f.is_source:
direct_files.append(f.short_path)

# Only collect workspace-internal labels and archives
if not str(target.label).startswith("@@") or str(target.label).startswith("@@//"):
# Collect .a archive files for baseline coverage.
for linker_input in target[CcInfo].linking_context.linker_inputs.to_list():
for lib in linker_input.libraries:
for archive in [lib.static_library, lib.pic_static_library]:
if archive and "/external/" not in archive.path and not archive.path.startswith("external/"):
direct_archives.append(archive)
break

# Propagate from children traversed by the aspect
for attr_name in ["components", "implementation", "deps", "implementation_deps", "exported_deps"]:
if hasattr(ctx.rule.attr, attr_name):
for dep in getattr(ctx.rule.attr, attr_name):
if _CoverageScopeInfo in dep:
transitive.append(dep[_CoverageScopeInfo].source_files)
transitive_archives.append(dep[_CoverageScopeInfo].object_files)

return [_CoverageScopeInfo(
source_files = depset(direct_files, transitive = transitive),
object_files = depset(direct_archives, transitive = transitive_archives),
)]

_coverage_scope_aspect = aspect(
implementation = _coverage_scope_aspect_impl,
attr_aspects = ["components", "implementation", "deps", "implementation_deps", "exported_deps"],
doc = "Traverses component/unit/cc_library hierarchy to collect implementation source files.",
)

# =============================================================================
# Rule: aggregates aspect results into an allowlist file
# =============================================================================

def _coverage_scope_impl(ctx):
print(ctx.configuration.short_id)
print(ctx.configuration.coverage_enabled)
"""Aggregates source file paths from all deps and writes allowlist + baseline objects."""
all_files = {}
all_objects = []

for dep in ctx.attr.deps:
if _CoverageScopeInfo in dep:
for path in dep[_CoverageScopeInfo].source_files.to_list():
if path:
all_files[path] = True
all_objects.append(dep[_CoverageScopeInfo].object_files)

sorted_files = sorted(all_files.keys())
object_depset = depset(transitive = all_objects)

# Write the allowlist file
output = ctx.actions.declare_file(ctx.attr.name + "_allowlist.txt")
ctx.actions.write(
output = output,
content = "\n".join(sorted_files) + "\n" if sorted_files else "",
)

# Write archive file paths for baseline coverage (reporter uses these as --object args)
archive_paths = sorted(set([f.short_path for f in object_depset.to_list()]))
objects_output = ctx.actions.declare_file(ctx.attr.name + "_objects.txt")
ctx.actions.write(
output = objects_output,
content = "\n".join(archive_paths) + "\n" if archive_paths else "",
)

return [
DefaultInfo(files = depset([output, objects_output], transitive = [object_depset])),
OutputGroupInfo(
allowlist = depset([output]),
objects = depset([objects_output]),
object_files = object_depset,
),
]

def _coverage_transition_impl(settings, attr):
# This dictionary modifies the build configuration
return {
"//command_line_option:collect_code_coverage": True,
}

# Define the transition
coverage_transition = transition(
implementation = _coverage_transition_impl,
inputs = [],
outputs = ["//command_line_option:collect_code_coverage"],
)

def _coverage_wrapper_impl(ctx):
# Forward the executable or providers from the underlying target
actual_target = ctx.attr.actual[0]
return [actual_target[DefaultInfo]]

# Define a rule that applies the transition to its 'actual' dependency
coverage_wrapper = rule(
implementation = _coverage_wrapper_impl,
attrs = {
"actual": attr.label(
mandatory = True,
cfg = coverage_transition, # Applying the transition here
),
# Mandatory attribute needed when a rule uses a transition
"_allowlist_function_transition": attr.label(
default = "@bazel_tools//tools/allowlists/function_transition_allowlist",
),
},
)
Comment thread
LittleHuba marked this conversation as resolved.

coverage_scope = rule(
implementation = _coverage_scope_impl,
doc = """Generates a file-level coverage allowlist from dependable_element targets.

Uses an aspect to traverse the component hierarchy of dependable_element
targets into their implementation cc_library targets (and transitive deps),
collecting all source files (srcs + hdrs). Outputs a text file with one
workspace-relative file path per line.

This allowlist is consumed by the coverage reporter to restrict coverage
reporting to exactly the source files that are part of a dependable_element.
""",
attrs = {
"deps": attr.label_list(
mandatory = True,
aspects = [_coverage_scope_aspect],
cfg = coverage_transition,
doc = "dependable_element _index targets whose implementation deps define the coverage scope.",
),
},
)
74 changes: 69 additions & 5 deletions quality/coverage/generate_coverage_html.sh
Original file line number Diff line number Diff line change
Expand Up @@ -117,12 +117,76 @@ else
# Copy the raw LCOV data for later archiving.
cp "${COVERAGE_REPORT}" "${TMPDIR_EXTRACT}/lcov.dat"

echo "Building collected coverage allowlist for QNX..."
# coverage_scope depends on Linux-only dependable element indices.
# Build it in the host/default config and reuse the workspace-relative allowlist for QNX filtering.
bazel build --config=qnx //quality/coverage:coverage_scope --output_groups=allowlist
ALLOWLIST_FILE="$(bazel info bazel-bin)/quality/coverage/coverage_scope_allowlist.txt"
Comment thread
LittleHuba marked this conversation as resolved.
if [[ ! -f "${ALLOWLIST_FILE}" ]]; then
echo "ERROR: Allowlist file not found at ${ALLOWLIST_FILE}" >&2
exit 1
fi

# Collect zero-coverage baseline LCOV records so that files not exercised by
# any test still appear in the HTML report with 0% coverage, rather than
# being silently omitted. Two sources contribute: (a) baseline_coverage.dat
# files placed alongside each coverage.dat by the Bazel coverage runner, and
# (b) _cc_coverage.dat files produced by dedicated baseline_coverage_test
# targets for C++ translation units that the GCOV runner handles separately.
BASELINE_LCOV="${TMPDIR_EXTRACT}/baseline_lcov.dat"
BASELINE_COUNT=0
LCOV_INPUT_LIST="${BUILD_WORKSPACE_DIRECTORY}/bazel-out/_coverage/lcov_files.tmp"
if [[ -f "${LCOV_INPUT_LIST}" ]]; then
# Collect baseline coverage next to each coverage.dat listed by Bazel.
# Some runners list baseline_coverage.dat directly, others only coverage.dat.
while IFS= read -r lcov_input; do
if [[ "${lcov_input}" == *"/baseline_coverage.dat" ]]; then
if [[ -f "${lcov_input}" ]]; then
printf "\n" >> "${BASELINE_LCOV}"
cat "${lcov_input}" >> "${BASELINE_LCOV}"
printf "\n" >> "${BASELINE_LCOV}"
BASELINE_COUNT=$((BASELINE_COUNT + 1))
fi
continue
fi
if [[ "${lcov_input}" == *"/coverage.dat" ]]; then
baseline_input="${lcov_input%coverage.dat}baseline_coverage.dat"
if [[ -f "${baseline_input}" ]]; then
printf "\n" >> "${BASELINE_LCOV}"
cat "${baseline_input}" >> "${BASELINE_LCOV}"
printf "\n" >> "${BASELINE_LCOV}"
BASELINE_COUNT=$((BASELINE_COUNT + 1))
fi
fi
done < <(sort -u "${LCOV_INPUT_LIST}")
fi
Comment thread
LittleHuba marked this conversation as resolved.

# Some baseline records with full DA line data are emitted through dedicated
# baseline_coverage_test targets and only exist as _cc_coverage.dat files.
# Merge them as additional baseline input.
while IFS= read -r cc_baseline; do
printf "\n" >> "${BASELINE_LCOV}"
cat "${cc_baseline}" >> "${BASELINE_LCOV}"
printf "\n" >> "${BASELINE_LCOV}"
BASELINE_COUNT=$((BASELINE_COUNT + 1))
done < <(find -L "${BUILD_WORKSPACE_DIRECTORY}/bazel-out" \
-path "*/testlogs/*/baseline_coverage_test/_coverage/_cc_coverage.dat" \
-type f | sort -u)

echo "Generating HTML report from LCOV data..."
bazel run //quality/coverage:lcov_to_html -- \
--lcov "${TMPDIR_EXTRACT}/lcov.dat" \
--output-dir "${OUTPUT_DIR}" \
--source-root "${BUILD_WORKSPACE_DIRECTORY}" \
--filter-regexes "${BUILD_WORKSPACE_DIRECTORY}/quality/coverage/llvm_cov/filter_regexes.txt"
LCOV_TO_HTML_ARGS=(
--lcov "${TMPDIR_EXTRACT}/lcov.dat"
--output-dir "${OUTPUT_DIR}"
--source-root "${BUILD_WORKSPACE_DIRECTORY}"
--allowlist "${ALLOWLIST_FILE}"
)

if [[ -s "${BASELINE_LCOV}" ]]; then
echo "Merging ${BASELINE_COUNT} baseline_coverage.dat files into report input..."
LCOV_TO_HTML_ARGS+=(--baseline-lcov "${BASELINE_LCOV}")
fi

bazel run //quality/coverage:lcov_to_html -- "${LCOV_TO_HTML_ARGS[@]}"
fi

echo "Coverage report written to: ${OUTPUT_DIR}"
Expand Down
Loading
Loading