From dac49ea52a5568d8d63f4e58d916797747d5d5d0 Mon Sep 17 00:00:00 2001 From: Dan Calavrezo <195309321+dcalavrezo-qorix@users.noreply.github.com> Date: Tue, 29 Sep 2026 16:29:55 +0300 Subject: [PATCH 1/2] Require the instrumentation filter; warn when it was left to Bazel baselibs' first QNX report showed score/os at 13 % against 80 % on Linux although the tests had run. Bazel guesses --instrumentation_filter from the packages of the test targets and strips only a trailing /tests; libraries tested from a "test" subpackage are outside the guess. On the gcov backend Bazel's collector then drops their counters; on both backends a library without an instrumented direct dependency is compiled without counters at all. - User manual: --instrumentation_filter=^//[/:] is part of the LLVM config (step 4) and required for the gcov config (step 4b). Architecture and known problems corrected: the filter is not irrelevant, it must name the module. - Both reporters warn when in-scope files without test data sit in a directory that is tested from a test/ or tests/ subdirectory and name the flag (tool_req__coverage_instrumentation_hint, ERR-13). - Integration workspace: //lib:cross_pkg tested from //lib/test, in both goldens; both configs set the filter; a gcov run with the guessed filter checks the warning. With the filter the vendored external header is measured on gcov too; golden and docs corrected. - Release notes 0.3.1; 0.3.0 dated. Signed-off-by: Dan Calavrezo <195309321+dcalavrezo-qorix@users.noreply.github.com> --- docs/architecture/index.rst | 12 +++-- docs/manual/known_problems.rst | 17 +++++-- docs/manual/user_manual.rst | 40 +++++++++++++++-- docs/release/release_notes.rst | 40 ++++++++++++++++- docs/requirements/potential_errors.rst | 15 ++++++- docs/requirements/tool_requirements.rst | 20 ++++++++- docs/verification/verification_report.rst | 11 ++--- integration_tests/.bazelrc | 10 +++++ integration_tests/expected_lcov.dat | 21 +++++++++ integration_tests/expected_lcov_gcov.dat | 41 ++++++++++++++--- integration_tests/lib/BUILD | 29 ++++++++++++ integration_tests/lib/cross_pkg.cpp | 26 +++++++++++ integration_tests/lib/cross_pkg.h | 23 ++++++++++ integration_tests/lib/test/BUILD | 19 ++++++++ integration_tests/lib/test/cross_pkg_test.cpp | 20 +++++++++ integration_tests/run_integration_test.sh | 31 ++++++++++--- integration_tests/tools/coverage/BUILD | 3 ++ score_coverage/gcov_reporter.py | 2 + score_coverage/reporter.py | 45 +++++++++++++++++++ score_coverage/tests/reporter_test.py | 30 +++++++++++++ 20 files changed, 426 insertions(+), 29 deletions(-) create mode 100644 integration_tests/lib/BUILD create mode 100644 integration_tests/lib/cross_pkg.cpp create mode 100644 integration_tests/lib/cross_pkg.h create mode 100644 integration_tests/lib/test/BUILD create mode 100644 integration_tests/lib/test/cross_pkg_test.cpp diff --git a/docs/architecture/index.rst b/docs/architecture/index.rst index ba9fdbf..78bda9b 100644 --- a/docs/architecture/index.rst +++ b/docs/architecture/index.rst @@ -236,9 +236,15 @@ consumer files. Design decisions ---------------- -- **Report-time filtering, not instrumentation filtering.** Instrumenting - everything and filtering by allowlist is what makes exact 0 % baselines - possible; ``--instrumentation_filter`` would hide untested files. +- **Report-time filtering on top of full instrumentation.** The scope + allowlist decides what the report shows; ``--instrumentation_filter`` is + set to the module's root package (``^//score[/:]``) so that every target is + compiled with counters. Bazel's guessed default covers only the packages of + the test targets: a target outside it is compiled without counters unless a + direct dep is instrumented (both backends), and on the gcov backend Bazel's + collector additionally drops the counters of every target outside the + filter. The reporters warn when files without test data sit in a directory + that is tested from a ``test/`` or ``tests/`` subdirectory (ERR-13). - **Fail loud, never fail green.** Every input problem ends in exit 2. The gate compares unrounded values and floors displayed percentages. - **Gate on the LCOV, not on llvm-cov's text summary.** The text summary omits diff --git a/docs/manual/known_problems.rst b/docs/manual/known_problems.rst index 0f8a1c8..40d7710 100644 --- a/docs/manual/known_problems.rst +++ b/docs/manual/known_problems.rst @@ -110,7 +110,16 @@ stay listed with their upstream references. - ``WARNING: N in-scope sources were not found`` in the reporter log, an index row without a link. - Report it; every declared source is expected to be exported. - * - **Instrumentation filter appears ignored.** - - ``--instrumentation_filter`` has no visible effect. - - Expected: ``--experimental_use_llvm_covmap`` instruments everything; - filtering happens at report time through the scope allowlist. + * - **Libraries tested from a** ``test`` **subpackage show 0 % or no-data.** + Bazel guesses ``--instrumentation_filter`` from the packages of the + test targets and strips only a trailing ``/tests``; ``score/os`` tested + from ``score/os/test`` is outside the guess. On the gcov backend Bazel's + collector then drops the library's counters; on both backends a library + without an instrumented direct dep is compiled without counters. + - Whole directories at 0 % on QNX that are covered on Linux; + ``WARNING: N in-scope files have no test data although their directory + is tested from a test/ or tests/ subdirectory`` in the reporter log; + ``Using default value for --instrumentation_filter`` in the Bazel + output. + - Set ``--instrumentation_filter=^//[/:]`` in every coverage + config (user manual, step 4). diff --git a/docs/manual/user_manual.rst b/docs/manual/user_manual.rst index a77bb91..f3379e2 100644 --- a/docs/manual/user_manual.rst +++ b/docs/manual/user_manual.rst @@ -176,6 +176,26 @@ Do **not** combine ``--config=llvm_cov`` with configs that append other ``--extra_toolchains`` (for example a GCC host config): the last toolchain wins resolution and a GCC toolchain produces no covmap data. +.. _instrumentation_filter: + +Set the instrumentation filter explicitly, to the module's root package: + +.. code-block:: text + + coverage:llvm_cov --instrumentation_filter=^//score[/:] + +Left unset, Bazel guesses the filter from the **packages of the test +targets** and strips only a trailing ``/tests`` (plural) to reach the code +under test; ``bazel coverage`` prints the guess as ``Using default value for +--instrumentation_filter``. A library whose tests live in a ``test`` +subpackage (``score/os`` tested from ``score/os/test``) is then outside the +filter. rules_cc still compiles it with counters when one of its direct deps +is instrumented, which hides the problem for most libraries, but a library +without such a dep is compiled without instrumentation and appears as +``no-data``. On the gcov backend the consequence is worse (see step 4b). The +reporters warn when the pattern is visible in the data (files without test +data whose directory is tested from a ``test/`` or ``tests/`` subdirectory). + Step 4b (optional): QNX on-target coverage, the gcov backend ------------------------------------------------------------ @@ -216,12 +236,20 @@ collector and points the final step at that reporter (copy and adapt the coverage:qnx --run_under=@score_qnx_unit_tests//src:run_under_qnx coverage:qnx --test_lang_filters=cc coverage:qnx --instrument_test_targets + coverage:qnx --instrumentation_filter=^//score[/:] coverage:qnx --noexperimental_use_llvm_covmap coverage:qnx --noexperimental_generate_llvm_lcov coverage:qnx --test_env=GENERATE_LLVM_LCOV --test_env=COVERAGE_GCOV_PATH --test_env=LLVM_PROFILE_CONTINUOUS_MODE coverage:qnx --coverage_output_generator=@bazel_tools//tools/test:lcov_merger coverage:qnx --coverage_report_generator=//tools/coverage:gcov_reporter_wrapper +The instrumentation filter is **required** on this backend, not only +advisable: Bazel's own collector converts counters only for the targets +inside the filter, so a library outside Bazel's guessed filter shows 0 % even +though its counters came back from the target (baselibs' ``score/os``: 13 % +on QNX against 80 % on Linux before the line was added; see +:ref:`instrumentation_filter`). + The LLVM-only rustc flags (``-Zcoverage-options=branch`` and friends) stay out of the way as long as they live in their own ``coverage:llvm_cov`` config, as in Step 4. If your workspace puts them on the bare ``coverage`` command @@ -240,10 +268,14 @@ Run and report as on Linux, with the platform filter for justifications: --yaml tools/coverage/coverage_justifications.yaml --archive-dir coverage_qnx_artifacts Known differences to the LLVM backend: gcov has no lines for unused inline -functions and for closing braces; headers a workspace target vendors from an -external repository are not measured (Bazel's collector filters them out) and -appear as ``no-data``; the report lists the toolchain's line semantics, not -LLVM's, so the two reports are compared per file, not merged. +functions and for closing braces; gcov counts every conditional jump the +compiler emits as a branch, including exception-handling edges, so branch +percentages are structurally lower than LLVM's; the report lists the +toolchain's line semantics, not LLVM's, so the two reports are compared per +file, not merged. Headers a workspace target vendors from an external +repository are measured as long as the vendoring target is inside the +instrumentation filter (Bazel's collector keeps the sources of instrumented +targets, external headers included). Step 5 (optional): justifications --------------------------------- diff --git a/docs/release/release_notes.rst b/docs/release/release_notes.rst index 62229fc..e266863 100644 --- a/docs/release/release_notes.rst +++ b/docs/release/release_notes.rst @@ -23,7 +23,45 @@ Release notes :security: NO :realizes: wp__module_sw_release_note -0.3.0 (unreleased) +0.3.1 (unreleased) +------------------ + +In plain words +~~~~~~~~~~~~~~ + +**Libraries tested from a** ``test`` **subpackage are measured on QNX.** The +first QNX report of baselibs showed ``score/os`` at 13 % where Linux showed +80 %. The tests had run; their counters were thrown away on the way back. +Bazel decides which packages take part in a coverage run by guessing from +where the tests are, and it does not look one directory up from a ``test`` +subpackage. On Linux our own merger ignores that guess; on QNX Bazel's own +collector obeys it. The fix is one line in the coverage config, +``--instrumentation_filter=^//score[/:]``, now required by the user manual +for both backends. The reporters warn when a report shows the pattern +(files without data in a directory that is tested from ``test/`` or +``tests/``), and the integration workspace contains such a library so the +case stays covered. + +Details for integrators +~~~~~~~~~~~~~~~~~~~~~~~ + +- Add ``--instrumentation_filter=^//[/:]`` to the + ``coverage:llvm_cov`` and QNX configs (user manual, steps 4 and 4b). Without + it a library without an instrumented direct dependency is compiled without + counters on both backends, and on the gcov backend every library outside + the guessed filter loses its counters. +- New warning in both reporters (``tool_req__coverage_instrumentation_hint``, + potential error ERR-13); ``known_problems`` and the architecture's design + decisions corrected: the filter is not irrelevant, it must name the module. +- Integration workspace: ``//lib:cross_pkg`` tested from ``//lib/test``, in + both goldens; a gcov run with the guessed filter checks the warning. +- Correction to the 0.3.0 notes: headers vendored from an external repository + **are** measured on the gcov backend once the vendoring target is inside + the instrumentation filter. The 0.3.0 statement described the guessed + filter, which left the vendoring target uninstrumented. The gcov golden of + the integration workspace now contains the vendored header. + +0.3.0 (2026-09-28) ------------------ In plain words diff --git a/docs/requirements/potential_errors.rst b/docs/requirements/potential_errors.rst index d12e7d8..c21b24c 100644 --- a/docs/requirements/potential_errors.rst +++ b/docs/requirements/potential_errors.rst @@ -135,6 +135,18 @@ requirements and constraints of use that mitigate the error. :need:`tool_req__coverage_gcov_baseline`, :need:`tool_req__coverage_report_unmapped`, :ref:`CSTR-08 ` + * - ERR-13 + - Both backends: Bazel's guessed ``--instrumentation_filter`` leaves a + library outside the instrumented set (its tests live in a ``test`` + subpackage). The library is compiled without counters, or on the gcov + backend its counters are dropped by Bazel's collector, and the report + shows 0 % or no-data instead of the measured coverage. + - low (coverage is under-reported, the safe direction; costs review + effort and hides the real state on QNX) + - good + - :need:`tool_req__coverage_instrumentation_hint`, + :need:`tool_req__coverage_validation_ground_truth`, + user manual step 4 (the filter line) Classification -------------- @@ -143,6 +155,7 @@ Tool impact: **yes**. An error in the *more coverage than real* direction lets a violation of the structural-coverage verification requirement go undetected. Tool error detection before qualification: **no** for ERR-02, ERR-03 and ERR-07; **weak** for ERR-11 and ERR-12 (a lost per-test record on QNX is -only noticed by comparing against the Linux report). The expected tool confidence level is therefore **TCL LOW**, and the +only noticed by comparing against the Linux report); ERR-13 under-reports and is +detected by the reporters' warning and the integration test. The expected tool confidence level is therefore **TCL LOW**, and the qualification method of the S-CORE process, validation of the software tool, applies. The evaluation itself is recorded in the Tool Verification Report. diff --git a/docs/requirements/tool_requirements.rst b/docs/requirements/tool_requirements.rst index fbd4141..df8a6b7 100644 --- a/docs/requirements/tool_requirements.rst +++ b/docs/requirements/tool_requirements.rst @@ -585,6 +585,20 @@ Summary and archive paths preserved, also when the gate fails; a missing test-logs directory shall be an error. +.. tool_req:: Instrumentation filter hint + :id: tool_req__coverage_instrumentation_hint + :version: 1 + :implemented: YES + :tags: report, ERR-13 + :safety: ASIL_B + :satisfies: stkh_req__coverage__uc_scope_completeness + + When an in-scope file has no test data, no file of its directory has test + data, and a ``test`` or ``tests`` subdirectory of that directory has, both + reporters shall warn that Bazel's default ``--instrumentation_filter`` + probably excluded the file and shall name the flag to set. The warning + shall list the files and shall not change the report. + Validation ---------- @@ -600,4 +614,8 @@ Validation C++ and Rust units whose line and branch counts are derived by hand: the produced LCOV shall match the expected records exactly (``DA``, ``BRDA``, ``LF``, ``LH``, ``BRF``, ``BRH`` per file), including exact-0 % records for an - untested C++ library and an untested Rust binary. + untested C++ library and an untested Rust binary. The fixture shall contain + a library in a package without tests that is exercised from a ``test`` + subpackage, measured on both backends with the explicit instrumentation + filter, and the gcov run shall be repeated with Bazel's guessed filter to + show the warning of :need:`tool_req__coverage_instrumentation_hint`. diff --git a/docs/verification/verification_report.rst b/docs/verification/verification_report.rst index 27de616..9b4caa1 100644 --- a/docs/verification/verification_report.rst +++ b/docs/verification/verification_report.rst @@ -53,10 +53,11 @@ Test inventory - 20 - merge_profraw, merge_no_data, merge_tool_error * - ``//score_coverage/tests:reporter_test`` - - 69 + - 71 - report_merged_profile, report_allowlist, report_baseline_zero, report_rlib_expansion, report_missing_baseline, report_relative_paths, - report_outputs, report_unmapped, scope_transitive + report_outputs, report_unmapped, scope_transitive, + instrumentation_hint * - ``//score_coverage/tests:gcov_reporter_test`` - 21 - gcov_merge, gcov_baseline, gcov_html, report_relative_paths, @@ -79,9 +80,9 @@ Test inventory * - ``//score_coverage/tests/starlark:coverage_scope_tests`` (14 analysis tests) - 14 - scope_transitive, scope_excludes, scope_baseline_objects, scope_gcno - * - ``integration_tests/run_integration_test.sh`` (22 end-to-end checks) - - 22 - - validation_ground_truth, report_baseline_zero, report_relative_paths, + * - ``integration_tests/run_integration_test.sh`` (25 end-to-end checks) + - 25 + - validation_ground_truth, instrumentation_hint, report_baseline_zero, report_relative_paths, report_allowlist, report_unmapped, gcov_merge, gcov_baseline, gcov_html, gate_exit_codes, gate_no_verdict, just_unknown_id, artifacts, summary_first diff --git a/integration_tests/.bazelrc b/integration_tests/.bazelrc index 53ac122..8dcf9f8 100644 --- a/integration_tests/.bazelrc +++ b/integration_tests/.bazelrc @@ -29,6 +29,12 @@ test --test_output=errors # ============================================================================ coverage:llvm_cov --nocache_test_results +# Instrument every package of this workspace. Left unset, Bazel guesses the +# filter from the packages of the test targets (stripping only a trailing +# "/tests"), and a library tested from a "test" subpackage, like //lib, is +# compiled without instrumentation unless one of its deps happens to be +# instrumented. A module uses its root package, e.g. ^//score[/:]. +coverage:llvm_cov --instrumentation_filter=^// coverage:llvm_cov --cxxopt=-O0 coverage:llvm_cov --combined_report=lcov coverage:llvm_cov --experimental_fetch_all_coverage_outputs @@ -82,6 +88,10 @@ coverage:llvm_cov --@rules_rust//rust/settings:extra_rustc_flag=-Zcoverage-optio # ============================================================================ # Use with: bazel coverage --config=gcov //src/... --build_tests_only coverage:gcov --nocache_test_results +# See coverage:llvm_cov. On this backend Bazel's own collector additionally +# drops the counters of every library outside the filter, even when it was +# compiled with instrumentation through an instrumented dependency. +coverage:gcov --instrumentation_filter=^// coverage:gcov --cxxopt=-O0 coverage:gcov --combined_report=lcov coverage:gcov --dynamic_mode=off diff --git a/integration_tests/expected_lcov.dat b/integration_tests/expected_lcov.dat index a354f1f..400ad83 100644 --- a/integration_tests/expected_lcov.dat +++ b/integration_tests/expected_lcov.dat @@ -21,6 +21,12 @@ # module's own lines (37-44) are instrumented and hit once. # Branches: 17 (value < 0) both ways, 19 (value == 0) true # only => 3 of 4. +# lib/cross_pkg.cpp library in a package without tests, exercised from +# //lib/test (the baselibs layout); only measured because the +# config sets --instrumentation_filter explicitly. pick(3) +# once: entry (18), condition (19), `return 0` (23) and the +# closing brace (24) hit; the `return 1` block (20-22) at 0; +# branch 19 false direction only => 1 of 2. # src/coverable.cpp classify() called with -5 and 0 => line 24 (`positive`) # at 0, branch 21 false direction never taken => 3 of 4. # rust/main.rs no test at all => every line 0 via the empty-profile @@ -90,6 +96,21 @@ BRH:0 LF:3 LH:0 end_of_record +SF:lib/cross_pkg.cpp +DA:18,1 +DA:19,1 +DA:20,0 +DA:21,0 +DA:22,0 +DA:23,1 +DA:24,1 +BRDA:19,0,0,0 +BRDA:19,0,1,1 +BRF:2 +BRH:1 +LF:7 +LH:4 +end_of_record SF:src/coverable.cpp DA:17,2 DA:18,2 diff --git a/integration_tests/expected_lcov_gcov.dat b/integration_tests/expected_lcov_gcov.dat index 60aa33d..31d84c9 100644 --- a/integration_tests/expected_lcov_gcov.dat +++ b/integration_tests/expected_lcov_gcov.dat @@ -13,12 +13,18 @@ # Ground truth for the gcov backend (GCC 12.2.0, the collection path QNX/QCC # takes on target), derived BY HAND from the fixture sources and # coverable_test, which is the only test of `bazel coverage --config=gcov -# //src/...`. Compared like expected_lcov.dat: function records are ignored, +# //src/... //lib/...`. Compared like expected_lcov.dat: function records are ignored, # one record per file. gcov semantics differ from LLVM's: a line is counted # when the compiler emitted code for it (no closing braces, no lines of # functions it never emitted), and BRDA lists both directions of every # conditional jump. # +# lib/cross_pkg.cpp library in a package without tests, exercised from +# //lib/test (the baselibs layout). Without the explicit +# --instrumentation_filter Bazel's collector drops its +# counters. pick(3) once: entry (17), condition (19) and +# `return 0` (23) hit, `return 1` (21) at 0; no brace lines; +# branch 19 false direction only => 1 of 2. # src/coverable.cpp classify() called with -5 and 0: entry (17) and the first # condition (18) twice; `negative` (19) once; the second # condition (21) and `zero` (22) once; `positive` (24) never. @@ -33,13 +39,38 @@ # gcov has no lines for 22-24 (the LLVM backend reports them # at 0). Reported under the declared path although the test # compiles it through the vendored_math_internal twin target. -# (absent) external/itest_external+/include/vext/vext.h: gcov records -# the header, but Bazel's per-test merger keeps only files of -# its instrumented-files manifest, which never lists sources -# of external repositories; the file is listed as no-data. +# external/itest_external+/include/vext/vext.h +# header vendored from an external repository by the +# workspace target //third_party:vendored_ext. Bazel's +# per-test merger keeps only files of the instrumented-files +# manifest, and that manifest lists the sources of the +# instrumented targets, external headers included: with the +# explicit --instrumentation_filter the vendoring target is +# instrumented and the header is measured (without the +# filter it was no-data). thrice() (18-19) called once by +# coverable_test; never_used() is an unused inline function +# with no code emitted => 2 of 2 lines, no branches. # (absent) src/empty_unit.cpp (compiled without code), src/unused_api.h # (no data), rust/*.rs (not instrumentable by gcov), # extlib.cpp / extlib.h (out of scope), the test source. +SF:external/itest_external+/include/vext/vext.h +DA:18,1 +DA:19,1 +LF:2 +LH:2 +end_of_record +SF:lib/cross_pkg.cpp +DA:17,1 +DA:19,1 +DA:21,0 +DA:23,1 +BRDA:19,0,0,0 +BRDA:19,0,1,1 +BRF:2 +BRH:1 +LF:4 +LH:3 +end_of_record SF:src/coverable.cpp DA:17,2 DA:18,2 diff --git a/integration_tests/lib/BUILD b/integration_tests/lib/BUILD new file mode 100644 index 0000000..7303f3d --- /dev/null +++ b/integration_tests/lib/BUILD @@ -0,0 +1,29 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* +load("@rules_cc//cc:cc_library.bzl", "cc_library") + +# A library in a package WITHOUT tests, exercised from the "test" subpackage +# (the baselibs layout: score/os is tested from score/os/test). Bazel guesses +# --instrumentation_filter from the packages of the test targets and strips +# only a trailing "/tests" (plural), so //lib is outside the guessed filter. +# No deps on purpose: a dependency on an instrumented target would get the +# library compiled with coverage anyway (rules_cc instruments a target whose +# direct dep is instrumented). Without an explicit filter this library shows +# 0 % on the gcov backend (its counters are dropped by Bazel's collector) and +# is missing on the LLVM backend (compiled without instrumentation). +cc_library( + name = "cross_pkg", + srcs = ["cross_pkg.cpp"], + hdrs = ["cross_pkg.h"], + visibility = ["//visibility:public"], +) diff --git a/integration_tests/lib/cross_pkg.cpp b/integration_tests/lib/cross_pkg.cpp new file mode 100644 index 0000000..efcf8b3 --- /dev/null +++ b/integration_tests/lib/cross_pkg.cpp @@ -0,0 +1,26 @@ +/******************************************************************************** + * 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 + *******************************************************************************/ +#include "lib/cross_pkg.h" + +namespace crosspkg { + +int pick(int value) +{ + if (value > 10) + { + return 1; + } + return 0; +} + +} // namespace crosspkg diff --git a/integration_tests/lib/cross_pkg.h b/integration_tests/lib/cross_pkg.h new file mode 100644 index 0000000..41d25c2 --- /dev/null +++ b/integration_tests/lib/cross_pkg.h @@ -0,0 +1,23 @@ +/******************************************************************************** + * 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 + *******************************************************************************/ +#ifndef INTEGRATION_LIB_CROSS_PKG_H +#define INTEGRATION_LIB_CROSS_PKG_H + +namespace crosspkg { + +// Returns 1 for values above ten, 0 otherwise. +int pick(int value); + +} // namespace crosspkg + +#endif // INTEGRATION_LIB_CROSS_PKG_H diff --git a/integration_tests/lib/test/BUILD b/integration_tests/lib/test/BUILD new file mode 100644 index 0000000..b48accd --- /dev/null +++ b/integration_tests/lib/test/BUILD @@ -0,0 +1,19 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* +load("@rules_cc//cc:cc_test.bzl", "cc_test") + +cc_test( + name = "cross_pkg_test", + srcs = ["cross_pkg_test.cpp"], + deps = ["//lib:cross_pkg"], +) diff --git a/integration_tests/lib/test/cross_pkg_test.cpp b/integration_tests/lib/test/cross_pkg_test.cpp new file mode 100644 index 0000000..8f1efac --- /dev/null +++ b/integration_tests/lib/test/cross_pkg_test.cpp @@ -0,0 +1,20 @@ +/******************************************************************************** + * 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 + *******************************************************************************/ +#include "lib/cross_pkg.h" + +// Calls pick() with a small value only: the `return 1` line and the true +// direction of the condition stay uncovered. +int main() +{ + return crosspkg::pick(3) == 0 ? 0 : 1; +} diff --git a/integration_tests/run_integration_test.sh b/integration_tests/run_integration_test.sh index c5c23ec..5cd3d2e 100755 --- a/integration_tests/run_integration_test.sh +++ b/integration_tests/run_integration_test.sh @@ -69,7 +69,7 @@ for marker in "## Coverage summary" "| Lines |" "Raw vs effective" \ "Coverage by directory" "Files at exact 0% (2)" \ "| In-scope files without coverage data | 1 |" \ "In-scope files without coverage data (1)" '- `src/unused_api.h`' \ - "Declaration-only headers (2)" "Compiled sources without code of their own (1)" '- `src/empty_unit.cpp`'; do + "Declaration-only headers (3)" "Compiled sources without code of their own (1)" '- `src/empty_unit.cpp`'; do if ! grep -qF -- "${marker}" summary.md; then echo "ERROR: '${marker}' missing from summary.md" >&2 exit 1 @@ -110,7 +110,7 @@ for f in artifacts_dir/coverage_linux/index.html artifacts_dir/coverage_report.d done # unused_api.h is the finding; coverable.h / uncovered.h hold declarations for # compiled .cpp files and empty_unit.cpp is a compiled placeholder: categorised. -EXPECTED_UNMAPPED=$'compiled-without-code\tsrc/empty_unit.cpp\ndeclaration-only\tsrc/coverable.h\ndeclaration-only\tsrc/uncovered.h\nno-data\tsrc/unused_api.h' +EXPECTED_UNMAPPED=$'compiled-without-code\tsrc/empty_unit.cpp\ndeclaration-only\tlib/cross_pkg.h\ndeclaration-only\tsrc/coverable.h\ndeclaration-only\tsrc/uncovered.h\nno-data\tsrc/unused_api.h' if [[ "$(cat artifacts_dir/unmapped_files.txt)" != "${EXPECTED_UNMAPPED}" ]]; then echo "ERROR: unmapped_files.txt unexpected:" >&2 cat artifacts_dir/unmapped_files.txt >&2 @@ -162,6 +162,13 @@ fi rm -f actual_normalised.dat expected_normalised.dat echo "OK: LCOV matches the ground truth" +echo "=== A library tested from a test/ subpackage must be measured (explicit --instrumentation_filter) ===" +# Bazel guesses the filter from the packages of the test targets; //lib is +# outside that guess and would be compiled without instrumentation. The +# config sets the filter explicitly; the golden above holds the numbers. +grep -q "^SF:lib/cross_pkg.cpp$" lcov.dat || { echo "ERROR: lib/cross_pkg.cpp missing: --instrumentation_filter not applied" >&2; exit 1; } +echo "OK: cross-package library measured on the LLVM backend" + echo "=== Every index link must point at an existing page; no machine or config paths ===" rm -rf link_check && mkdir link_check unzip -q coverage_artifacts.zip -d link_check @@ -295,7 +302,7 @@ echo "OK: unknown justification id is reported and does not count" # transport of score_qnx_unit_tests differs. # --------------------------------------------------------------------------- echo "=== gcov backend: coverage build with the GCC toolchain ===" -bazel coverage --config=gcov //src/... --build_tests_only +bazel coverage --config=gcov //src/... //lib/... --build_tests_only echo "=== gcov backend: gate, HTML, archive ===" rm -rf gcov_artifacts_dir @@ -328,7 +335,7 @@ GLINKS="$(grep -oE 'href="index\.[^"]+\.html"' "${GHTML}/index.html" | sed -E 's while IFS= read -r link; do [[ -f "${GHTML}/${link}" ]] || { echo "ERROR: gcovr index links to ${link}, which does not exist" >&2; exit 1; } done <<< "${GLINKS}" -for page in coverable.cpp uncovered.cpp inline_math.h; do +for page in coverable.cpp uncovered.cpp inline_math.h cross_pkg.cpp; do ls "${GHTML}"/index."${page}".*.html > /dev/null 2>&1 || { echo "ERROR: no gcovr page for ${page}" >&2; exit 1; } done G_RAW="$(grep -oP 'Raw line coverage:\s+\K[0-9.]+' gcov_artifacts_dir/justification_report/summary.txt)" @@ -337,7 +344,7 @@ if ! awk "BEGIN {exit (${G_EFF} > ${G_RAW}) ? 0 : 1}"; then echo "ERROR: gcov effective coverage ${G_EFF}% not above raw ${G_RAW}% (justification not applied on gcovr HTML)" >&2 exit 1 fi -EXPECTED_GCOV_UNMAPPED=$'compiled-without-code\tsrc/empty_unit.cpp\ndeclaration-only\tsrc/coverable.h\ndeclaration-only\tsrc/uncovered.h\nno-data\texternal/itest_external+/include/vext/vext.h\nno-data\tsrc/unused_api.h\nnot-instrumented\trust/lib.rs\nnot-instrumented\trust/main.rs' +EXPECTED_GCOV_UNMAPPED=$'compiled-without-code\tsrc/empty_unit.cpp\ndeclaration-only\tlib/cross_pkg.h\ndeclaration-only\tsrc/coverable.h\ndeclaration-only\tsrc/uncovered.h\nno-data\tsrc/unused_api.h\nnot-instrumented\trust/lib.rs\nnot-instrumented\trust/main.rs' if [[ "$(cat gcov_artifacts_dir/unmapped_files.txt)" != "${EXPECTED_GCOV_UNMAPPED}" ]]; then echo "ERROR: gcov unmapped_files.txt unexpected:" >&2 cat gcov_artifacts_dir/unmapped_files.txt >&2 @@ -347,5 +354,19 @@ grep -qF "Not instrumentable by this backend (2)" gcov_summary.md || { echo "ERR rm -rf gcov_artifacts_dir gcov_summary.md gcov_run.log coverage_gcov echo "OK: gcov HTML complete (${GLINKS//$'\n'/, }), effective ${G_EFF}% > raw ${G_RAW}%, categories as expected" +echo "=== gcov backend: without the explicit filter the reporter must point at --instrumentation_filter ===" +# Bazel's guessed filter for these targets is ^//lib/test[/:],^//src[/:]; pass +# it explicitly to reproduce a consumer config that forgot the flag. Bazel's +# collector then drops lib/cross_pkg.cpp's counters; the file falls back to +# the 0 % baseline and the reporter must name the cause. +bazel coverage --config=gcov '--instrumentation_filter=^//lib/test[/:],^//src[/:]' //src/... //lib/... --build_tests_only > gcov_narrow.log 2>&1 || { cat gcov_narrow.log; exit 1; } +grep -q "WARNING: 1 in-scope files have no test data although their directory is tested from a test/ or tests/ subdirectory" gcov_narrow.log \ + || { cat gcov_narrow.log; echo "ERROR: the reporter did not warn about the narrow --instrumentation_filter" >&2; exit 1; } +grep -q -- "--instrumentation_filter=\^//\[/:\]" gcov_narrow.log || { echo "ERROR: the warning does not name the flag to set" >&2; exit 1; } +unzip -p bazel-out/_coverage/_coverage_report.dat lcov_report/lcov.dat | awk '/^SF:lib\/cross_pkg.cpp$/{p=1} p&&/^LH:/{print; exit}' | grep -q "^LH:0$" \ + || { echo "ERROR: expected lib/cross_pkg.cpp at 0 % under the narrow filter" >&2; exit 1; } +rm -f gcov_narrow.log +echo "OK: narrow --instrumentation_filter is detected and reported" + echo "" echo "=== All integration checks passed ===" diff --git a/integration_tests/tools/coverage/BUILD b/integration_tests/tools/coverage/BUILD index a3238c4..372eb7f 100644 --- a/integration_tests/tools/coverage/BUILD +++ b/integration_tests/tools/coverage/BUILD @@ -21,6 +21,9 @@ score_coverage_scope( testonly = True, visibility = ["//visibility:private"], deps = [ + # Library in a package without tests, tested from //lib/test + # (baselibs layout): needs the explicit --instrumentation_filter. + "//lib:cross_pkg", "//rust:integration_lib", "//rust:untested_tool", "//src:coverable", diff --git a/score_coverage/gcov_reporter.py b/score_coverage/gcov_reporter.py index 133dd35..6719789 100644 --- a/score_coverage/gcov_reporter.py +++ b/score_coverage/gcov_reporter.py @@ -53,6 +53,7 @@ resolve_tool, select_files, stage_sources, + warn_instrumentation_filter, write_empty_output, ) @@ -155,6 +156,7 @@ def main(argv: list[str] | None = None) -> None: baseline_covered = {name: name for name in baseline} compiled_stems = compiled_stems_from_gcno(list(gcno_files.values())) selection = select_files(test_covered, baseline_covered, allowlist, compiled_stems) + warn_instrumentation_filter(selection, set(tested)) not_instrumented = mark_not_instrumented(selection) report_selection(selection, not_instrumented) diff --git a/score_coverage/reporter.py b/score_coverage/reporter.py index 492139e..219023d 100644 --- a/score_coverage/reporter.py +++ b/score_coverage/reporter.py @@ -393,6 +393,50 @@ def in_scope(name: str) -> bool: ) +_TEST_SUBDIRECTORIES = ("test", "tests") + + +def instrumentation_filter_suspects(no_data: set[str], with_data: set[str]) -> list[str]: + """In-scope files without test data whose directory is tested from a ``test``/``tests`` subdirectory. + + Bazel guesses ``--instrumentation_filter`` from the packages of the test + targets and strips only a trailing ``/tests``. A library tested from a + ``test`` subpackage is then outside the filter: compiled without counters + (both backends) or, on the gcov backend, its counters are dropped by + Bazel's collector. Such files show 0 % or no data although a test ran + them. The heuristic: no file of the directory has test data, while a + ``test/`` or ``tests/`` subdirectory of it has. + """ + dirs_with_data = {name.rsplit("/", 1)[0] if "/" in name else "" for name in with_data} + suspects = [] + for name in no_data: + directory = name.rsplit("/", 1)[0] if "/" in name else "" + if directory in dirs_with_data: + continue + tested_from_subdir = any( + d == f"{directory}/{sub}" or d.startswith(f"{directory}/{sub}/") + for d in dirs_with_data + for sub in _TEST_SUBDIRECTORIES + ) + if tested_from_subdir: + suspects.append(name) + return sorted(suspects) + + +def warn_instrumentation_filter(selection: FileSelection, with_data: set[str]) -> list[str]: + """Print the ``--instrumentation_filter`` hint for the suspects; returns them.""" + suspects = instrumentation_filter_suspects(selection.baseline_only | selection.unmapped, with_data) + if suspects: + print( + f"WARNING: {len(suspects)} in-scope files have no test data although their directory is tested " + f"from a test/ or tests/ subdirectory (e.g., {suspects[:5]}). Bazel's default " + "--instrumentation_filter covers only the packages of the test targets; set " + "--instrumentation_filter=^//[/:] in the coverage config (user manual, step 4).", + file=sys.stderr, + ) + return suspects + + UNMAPPED_NO_DATA = "no-data" UNMAPPED_DECLARATION_ONLY = "declaration-only" UNMAPPED_EMPTY_UNIT = "compiled-without-code" @@ -716,6 +760,7 @@ def prepare_sources( _attribute_foreign_virtual_includes(test_covered, baseline_covered, allowlist_set, path_map) selection = select_files(test_covered, baseline_covered, allowlist_set, compiled_stems) + warn_instrumentation_filter(selection, set(test_covered.values())) for name, dropped in sorted(selection.duplicates.items()): print( f"WARNING: {name} is compiled under several paths; reporting one, dropping {sorted(dropped)}", diff --git a/score_coverage/tests/reporter_test.py b/score_coverage/tests/reporter_test.py index 6a92c36..6c9f68c 100644 --- a/score_coverage/tests/reporter_test.py +++ b/score_coverage/tests/reporter_test.py @@ -843,6 +843,36 @@ def test_virtual_path_maps_to_declared_header_under_any_config(self): "src/v/include/api.h", ) + def test_instrumentation_filter_suspects_flags_libraries_tested_from_a_test_subdirectory(self): + # score/os tested from score/os/test (baselibs layout): no file of + # score/os has data, the test sources below score/os/test have. + with_data = {"score/os/test/acl_test.cpp", "score/os/utils/acl/acl.cpp", "score/json/tests/j_test.cpp"} + no_data = { + "score/os/acl_impl.cpp", # suspect: tested from score/os/test/ + "score/json/model.cpp", # suspect: tested from score/json/tests/ + "score/os/utils/acl/other.cpp", # sibling has data: an ordinary untested file + "score/net/socket.cpp", # nothing tested below it: ordinary untested file + } + self.assertEqual( + reporter.instrumentation_filter_suspects(no_data, with_data), + ["score/json/model.cpp", "score/os/acl_impl.cpp"], + ) + self.assertEqual(reporter.instrumentation_filter_suspects(set(), with_data), []) + + def test_warn_instrumentation_filter_names_the_flag(self): + selection = reporter.FileSelection(baseline_only={"score/os/acl_impl.cpp"}, unmapped={"score/os/glob.cpp"}) + err = io.StringIO() + with redirect_stderr(err): + suspects = reporter.warn_instrumentation_filter(selection, {"score/os/test/acl_test.cpp"}) + self.assertEqual(suspects, ["score/os/acl_impl.cpp", "score/os/glob.cpp"]) + self.assertIn("--instrumentation_filter=^//[/:]", err.getvalue()) + self.assertIn("2 in-scope files", err.getvalue()) + # Nothing to say when the directory itself has data. + err = io.StringIO() + with redirect_stderr(err): + self.assertEqual(reporter.warn_instrumentation_filter(selection, {"score/os/errno.cpp"}), []) + self.assertEqual(err.getvalue(), "") + def test_unmapped_virtual_path_keeps_its_config_free_form(self): self.assertEqual( reporter.canonical_path("bazel-out/k8-fastbuild/bin/src/_virtual_includes/u/y.h", self.MAP), From 9abd91fffaca028b7fb21c45adab47b4f8cdf174 Mon Sep 17 00:00:00 2001 From: Dan Calavrezo <195309321+dcalavrezo-qorix@users.noreply.github.com> Date: Tue, 29 Sep 2026 16:37:46 +0300 Subject: [PATCH 2/2] Manual: give the instrumentation-filter reference a title The label precedes a paragraph, not a heading, so the bare :ref: had no caption and the docs job (warnings as errors) failed. Signed-off-by: Dan Calavrezo <195309321+dcalavrezo-qorix@users.noreply.github.com> --- docs/manual/user_manual.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/manual/user_manual.rst b/docs/manual/user_manual.rst index f3379e2..c73b6b0 100644 --- a/docs/manual/user_manual.rst +++ b/docs/manual/user_manual.rst @@ -248,7 +248,7 @@ advisable: Bazel's own collector converts counters only for the targets inside the filter, so a library outside Bazel's guessed filter shows 0 % even though its counters came back from the target (baselibs' ``score/os``: 13 % on QNX against 80 % on Linux before the line was added; see -:ref:`instrumentation_filter`). +:ref:`the instrumentation filter `). The LLVM-only rustc flags (``-Zcoverage-options=branch`` and friends) stay out of the way as long as they live in their own ``coverage:llvm_cov`` config,