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/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 cd813196b5..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,7 +198,14 @@ 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. + 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 " + + "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( 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..12351affde --- /dev/null +++ b/tests/coverage_deps/coverage_deps_test.bzl @@ -0,0 +1,95 @@ +# 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) + +# 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. + + Args: + name: the name of the test suite. + """ + test_suite(name = name, basic_tests = _tests)