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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions python/private/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ bzl_library(
srcs = ["coverage_deps.bzl"],
deps = [
":bazel_tools_bzl",
":repo_utils_bzl",
":version_label_bzl",
],
)
Expand Down Expand Up @@ -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",
Expand Down
21 changes: 19 additions & 2 deletions python/private/coverage_deps.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -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 <version>'
Expand Down Expand Up @@ -166,18 +167,27 @@ _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:
name: The name of the registered repository.
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
Expand All @@ -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(
Expand Down
1 change: 1 addition & 0 deletions python/private/python.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
15 changes: 15 additions & 0 deletions python/private/python_register_toolchains.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
17 changes: 17 additions & 0 deletions tests/coverage_deps/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -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")
95 changes: 95 additions & 0 deletions tests/coverage_deps/coverage_deps_test.bzl
Original file line number Diff line number Diff line change
@@ -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)