From 4a5464a0c466c5cce21c47b2c6dfa12cf4aa20ba Mon Sep 17 00:00:00 2001 From: Joshua Yanchar Date: Sun, 10 May 2026 10:24:26 -0700 Subject: [PATCH 1/3] feat(coverage): warn when bundled coverage tool has no wheel for requested python_version/platform Previously, when `configure_coverage_tool = True` was set but the bundled `coverage.py` wheel set had no entry for the requested (python_version, platform), `coverage_dep` returned None silently. The result was that `bazel coverage` produced empty per-test lcov files for `py_test` targets with no signal to the user that coverage was unconfigured. Print a WARNING in that path so the misconfiguration is visible. Preserve the existing silent return for the windows branch, which is intentionally quiet because the upstream coverage wrapper does not support windows. --- CHANGELOG.md | 4 ++++ python/private/coverage_deps.bzl | 10 +++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3feaebf428..db6edd97d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -65,6 +65,10 @@ END_UNRELEASED_TEMPLATE default to `true`. * (pypi) The data files of a wheel (bin, includes, etc) are now always included as a library's data dependencies. +* (coverage) When `configure_coverage_tool = True` is set but the bundled + `coverage.py` wheel set has no entry for the requested python version and + platform, a warning is now printed instead of silently producing an empty + coverage report. {#v0-0-0-fixed} ### Fixed diff --git a/python/private/coverage_deps.bzl b/python/private/coverage_deps.bzl index cd813196b5..46fd51e861 100644 --- a/python/private/coverage_deps.bzl +++ b/python/private/coverage_deps.bzl @@ -188,7 +188,15 @@ def coverage_dep(name, python_version, platform, visibility): url, sha256 = _coverage_deps.get(abi, {}).get(platform, (None, "")) if url == None: - # Some wheels are not present for some builds, so let's silently ignore those. + # buildifier: disable=print + print(( + "WARNING: rules_python's bundled coverage tool has no wheel for " + + "python_version={}, platform={}. `bazel coverage` will produce " + + "empty lcov for py_test targets in this configuration. Either " + + "pin python_version to a version in the bundled set (see " + + "python/private/coverage_deps.bzl), or configure coverage " + + "manually via py_runtime.coverage_tool. See docs/coverage.md." + ).format(python_version, platform)) return None maybe( From afdc0de093418601c946f17f39739ba6ffe9c225 Mon Sep 17 00:00:00 2001 From: Joshua Yanchar Date: Wed, 13 May 2026 16:34:20 -0700 Subject: [PATCH 2/3] review: use repo_utils.logger.warn instead of print Per review on #3766, replace the raw `print(...)` warning with `logger.warn(...)` from `repo_utils.logger`, with the logger threaded in from the caller. - `coverage_dep` takes an optional `logger` parameter and falls back to a default-constructed logger if none is supplied. - `python_register_toolchains` accepts a private `_internal_module_ctx` kwarg (mirroring the existing `_internal_bzlmod_toolchain_call` pattern). When invoked from the bzlmod path, it builds the logger with the real `module_ctx` so module-root filtering applies (see https://github.com/bazel-contrib/rules_python/pull/3760). For the WORKSPACE/macro path it constructs a minimal stand-in struct, which is all the logger needs. - `python.bzl` passes `module_ctx` through. Adds tests/coverage_deps/ with two cases (unsupported version warns, windows stays silent) using the captured-printer pattern already established in tests/pypi/hub_builder/hub_builder_tests.bzl. --- python/private/BUILD.bazel | 2 + python/private/coverage_deps.bzl | 17 +++- python/private/python.bzl | 1 + python/private/python_register_toolchains.bzl | 15 ++++ tests/coverage_deps/BUILD.bazel | 17 ++++ tests/coverage_deps/coverage_deps_test.bzl | 84 +++++++++++++++++++ 6 files changed, 132 insertions(+), 4 deletions(-) create mode 100644 tests/coverage_deps/BUILD.bazel create mode 100644 tests/coverage_deps/coverage_deps_test.bzl diff --git a/python/private/BUILD.bazel b/python/private/BUILD.bazel index db96957724..6ab3d546be 100644 --- a/python/private/BUILD.bazel +++ b/python/private/BUILD.bazel @@ -175,6 +175,7 @@ bzl_library( srcs = ["coverage_deps.bzl"], deps = [ ":bazel_tools_bzl", + ":repo_utils_bzl", ":version_label_bzl", ], ) @@ -293,6 +294,7 @@ bzl_library( ":full_version_bzl", ":internal_config_repo_bzl", ":python_repository_bzl", + ":repo_utils_bzl", ":toolchains_repo_bzl", "//python:versions_bzl", "//python/private/pypi:deps_bzl", diff --git a/python/private/coverage_deps.bzl b/python/private/coverage_deps.bzl index 46fd51e861..a32b2e3f97 100644 --- a/python/private/coverage_deps.bzl +++ b/python/private/coverage_deps.bzl @@ -17,6 +17,7 @@ load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") load("@bazel_tools//tools/build_defs/repo:utils.bzl", "maybe") +load("//python/private:repo_utils.bzl", "repo_utils") load("//python/private:version_label.bzl", "version_label") # START: maintained by 'bazel run //tools/private/update_deps:update_coverage_deps ' @@ -166,7 +167,7 @@ _coverage_deps = { _coverage_patch = Label("//python/private:coverage.patch") -def coverage_dep(name, python_version, platform, visibility): +def coverage_dep(name, python_version, platform, visibility, logger = None): """Register a single coverage dependency based on the python version and platform. Args: @@ -174,10 +175,19 @@ def coverage_dep(name, python_version, platform, visibility): python_version: The full python version. platform: The platform, which can be found in //python:versions.bzl PLATFORMS dict. visibility: The visibility of the coverage tool. + logger: {type}`repo_utils.logger | None` Optional logger used to emit a + warning when no wheel is available for the (python_version, + platform) pair. If not supplied, a default logger is constructed. Returns: The label of the coverage tool if the platform is supported, otherwise - None. """ + if logger == None: + logger = repo_utils.logger( + struct(getenv = lambda _: None), + name = "coverage_dep", + ) + if "windows" in platform: # NOTE @aignas 2023-01-19: currently we do not support windows as the # upstream coverage wrapper is written in shell. Do not log any warning @@ -188,9 +198,8 @@ def coverage_dep(name, python_version, platform, visibility): url, sha256 = _coverage_deps.get(abi, {}).get(platform, (None, "")) if url == None: - # buildifier: disable=print - print(( - "WARNING: rules_python's bundled coverage tool has no wheel for " + + logger.warn(lambda: ( + "rules_python's bundled coverage tool has no wheel for " + "python_version={}, platform={}. `bazel coverage` will produce " + "empty lcov for py_test targets in this configuration. Either " + "pin python_version to a version in the bundled set (see " + diff --git a/python/private/python.bzl b/python/private/python.bzl index 0f12f88f0c..6abc81e3d2 100644 --- a/python/private/python.bzl +++ b/python/private/python.bzl @@ -275,6 +275,7 @@ def _python_impl(module_ctx): register_result = python_register_toolchains( name = toolchain_info.name, _internal_bzlmod_toolchain_call = True, + _internal_module_ctx = module_ctx, **kwargs ) if not register_result.impl_repos: diff --git a/python/private/python_register_toolchains.bzl b/python/private/python_register_toolchains.bzl index 9e75c41978..3b92902c7e 100644 --- a/python/private/python_register_toolchains.bzl +++ b/python/private/python_register_toolchains.bzl @@ -26,6 +26,7 @@ load( load(":coverage_deps.bzl", "coverage_dep") load(":full_version.bzl", "full_version") load(":python_repository.bzl", "python_repository") +load(":repo_utils.bzl", "repo_utils") load( ":toolchains_repo.bzl", "host_compatible_python_repo", @@ -89,6 +90,19 @@ def python_register_toolchains( if bzlmod_toolchain_call: register_toolchains = False + # When invoked from the bzlmod python extension, a module_ctx is plumbed in + # so the coverage_dep logger can attribute warnings to the right module and + # honor module-root filtering. In the WORKSPACE/macro path no module_ctx is + # available; a minimal stand-in struct gives the logger what it needs. + module_ctx = kwargs.pop("_internal_module_ctx", None) + if module_ctx != None: + coverage_logger = repo_utils.logger(module_ctx, name = "coverage_dep") + else: + coverage_logger = repo_utils.logger( + struct(getenv = lambda _: None), + name = "coverage_dep", + ) + base_url = kwargs.pop("base_url", DEFAULT_RELEASE_BASE_URL) tool_versions = tool_versions or TOOL_VERSIONS minor_mapping = minor_mapping or MINOR_MAPPING @@ -121,6 +135,7 @@ def python_register_toolchains( ), python_version = python_version, platform = platform, + logger = coverage_logger, visibility = ["@{name}_{platform}//:__subpackages__".format( name = name, platform = platform, diff --git a/tests/coverage_deps/BUILD.bazel b/tests/coverage_deps/BUILD.bazel new file mode 100644 index 0000000000..8ec6025902 --- /dev/null +++ b/tests/coverage_deps/BUILD.bazel @@ -0,0 +1,17 @@ +# 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. + +load(":coverage_deps_test.bzl", "coverage_deps_test_suite") + +coverage_deps_test_suite(name = "coverage_deps_tests") diff --git a/tests/coverage_deps/coverage_deps_test.bzl b/tests/coverage_deps/coverage_deps_test.bzl new file mode 100644 index 0000000000..d6ade161ce --- /dev/null +++ b/tests/coverage_deps/coverage_deps_test.bzl @@ -0,0 +1,84 @@ +# 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. + +"Tests for the warning emitted by coverage_dep when no wheel is available." + +load("@rules_testing//lib:test_suite.bzl", "test_suite") +load("//python/private:coverage_deps.bzl", "coverage_dep") # buildifier: disable=bzl-visibility +load("//python/private:repo_utils.bzl", "REPO_DEBUG_ENV_VAR", "REPO_VERBOSITY_ENV_VAR", "repo_utils") # buildifier: disable=bzl-visibility + +_tests = [] + +def _capturing_logger(): + """Build a (logger, captured_messages_list) pair. + + The logger has its verbosity set to INFO so WARN messages are captured but + nothing noisier than necessary is emitted. The printer collects the second + positional argument from each printer invocation (the formatted message). + """ + captured = [] + logger = repo_utils.logger( + struct( + getenv = { + REPO_DEBUG_ENV_VAR: None, + REPO_VERBOSITY_ENV_VAR: "INFO", + }.get, + ), + name = "unit-test", + printer = lambda _key, message: captured.append(message), + ) + return logger, captured + +def _test_unsupported_python_version_warns(env): + # cp37 is not in the bundled wheel set; coverage_dep should return None + # and emit a warning describing the misconfiguration. + logger, captured = _capturing_logger() + result = coverage_dep( + name = "unused_for_test", + python_version = "3.7", + platform = "aarch64-apple-darwin", + visibility = ["//visibility:public"], + logger = logger, + ) + env.expect.that_bool(result == None).equals(True) + env.expect.that_int(len(captured)).equals(1) + env.expect.that_str(captured[0]).contains("no wheel for") + env.expect.that_str(captured[0]).contains("python_version=3.7") + env.expect.that_str(captured[0]).contains("platform=aarch64-apple-darwin") + +_tests.append(_test_unsupported_python_version_warns) + +def _test_windows_platform_is_silent(env): + # Windows is intentionally unsupported and not actionable; coverage_dep + # must return None without logging anything. + logger, captured = _capturing_logger() + result = coverage_dep( + name = "unused_for_test", + python_version = "3.10", + platform = "x86_64-pc-windows-msvc", + visibility = ["//visibility:public"], + logger = logger, + ) + env.expect.that_bool(result == None).equals(True) + env.expect.that_int(len(captured)).equals(0) + +_tests.append(_test_windows_platform_is_silent) + +def coverage_deps_test_suite(name): + """Create the test suite. + + Args: + name: the name of the test suite. + """ + test_suite(name = name, basic_tests = _tests) From 4686ae072f032a85e4306fefc948601ed51e008c Mon Sep 17 00:00:00 2001 From: Joshua Yanchar Date: Wed, 13 May 2026 16:52:24 -0700 Subject: [PATCH 3/3] test(coverage): document why the supported-wheel path has no unit test Add a comment explaining that the supported-wheel path of coverage_dep calls maybe(http_archive, ...) which calls native.existing_rule(), which is only valid during BUILD/macro/finalizer evaluation, not during rule analysis where rules_testing analysis tests run. The path is covered end-to-end by real bazel coverage runs. --- tests/coverage_deps/coverage_deps_test.bzl | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/coverage_deps/coverage_deps_test.bzl b/tests/coverage_deps/coverage_deps_test.bzl index d6ade161ce..12351affde 100644 --- a/tests/coverage_deps/coverage_deps_test.bzl +++ b/tests/coverage_deps/coverage_deps_test.bzl @@ -75,6 +75,17 @@ def _test_windows_platform_is_silent(env): _tests.append(_test_windows_platform_is_silent) +# NOTE: there is intentionally no unit test for the supported-wheel path +# (where coverage_dep returns a non-None label and emits no warning). +# That path calls `maybe(http_archive, ...)`, which calls +# `native.existing_rule()`. `native.existing_rule()` is only valid during +# BUILD file, legacy macro, or rule finalizer evaluation -- not during +# rule analysis, which is the phase rules_testing analysis tests run in. +# Calling coverage_dep with supported args from here therefore fails with +# "existing_rule() can only be used while evaluating a BUILD file, ...". +# The supported-wheel path is exercised end-to-end by `bazel coverage` +# against a real py_test target during ordinary use of the toolchain. + def coverage_deps_test_suite(name): """Create the test suite.