Skip to content

Commit 5a16df4

Browse files
committed
POC for py_test toolchain
POC
1 parent 797cbe8 commit 5a16df4

13 files changed

+221
-10
lines changed

examples/bzlmod/.coveragerc

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
[report]
2+
include_namespace_packages=True
3+
skip_covered=True
4+
[run]
5+
relative_files=True
6+
branch=True

examples/bzlmod/BUILD.bazel

+2
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ load("@python_3_9//:defs.bzl", py_test_with_transition = "py_test")
1111
load("@python_versions//3.10:defs.bzl", compile_pip_requirements_3_10 = "compile_pip_requirements")
1212
load("@rules_python//python:defs.bzl", "py_binary", "py_library", "py_test")
1313

14+
exports_files([".coveragerc"])
15+
1416
# This stanza calls a rule that generates targets for managing pip dependencies
1517
# with pip-compile for a particular python version.
1618
compile_pip_requirements_3_10(

examples/bzlmod/MODULE.bazel

+7-1
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@ python.toolchain(
3636
configure_coverage_tool = True,
3737
python_version = "3.10",
3838
)
39+
python.converage(
40+
name = "coverage",
41+
coveragerc = ".coveragerc",
42+
)
3943

4044
# One can override the actual toolchain versions that are available, which can be useful
4145
# when optimizing what gets downloaded and when.
@@ -89,7 +93,9 @@ python.single_version_platform_override(
8993
# See the tests folder for various examples on using multiple Python versions.
9094
# The names "python_3_9" and "python_3_10" are autmatically created by the repo
9195
# rules based on the `python_version` arg values.
92-
use_repo(python, "python_3_10", "python_3_9", "python_versions", "pythons_hub")
96+
use_repo(python, "coverage_py_test_toolchain", "python_3_10", "python_3_9", "python_versions", "pythons_hub")
97+
98+
register_toolchains("@coverage_py_test_toolchain//:all")
9399

94100
# EXPERIMENTAL: This is experimental and may be removed without notice
95101
uv = use_extension("@rules_python//python/uv:extensions.bzl", "uv")

examples/bzlmod/tests/BUILD.bazel

+7
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,12 @@ py_test(
4848
deps = ["//libs/my_lib"],
4949
)
5050

51+
py_test(
52+
name = "coverage_rc_is_set_test",
53+
srcs = ["coverage_rc_is_set_test.py"],
54+
main = "coverage_rc_is_set_test.py",
55+
)
56+
5157
py_test_3_9(
5258
name = "my_lib_3_9_test",
5359
srcs = ["my_lib_test.py"],
@@ -58,6 +64,7 @@ py_test_3_9(
5864
py_test_3_10(
5965
name = "my_lib_3_10_test",
6066
srcs = ["my_lib_test.py"],
67+
env = {"PYTHONPATH": "$(location //libs/my_lib)"},
6168
main = "my_lib_test.py",
6269
deps = ["//libs/my_lib"],
6370
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# Copyright 2024 The Bazel Authors. All rights reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
import os
15+
import tempfile
16+
import unittest
17+
18+
19+
class TestEnvironmentVariables(unittest.TestCase):
20+
def test_coverage_rc_file_exists(self):
21+
# Assert that the environment variable is set and points to a valid file
22+
coverage_rc_path = os.environ.get("COVERAGE_RC")
23+
self.assertTrue(
24+
os.path.isfile(coverage_rc_path),
25+
"COVERAGE_RC does not point to a valid file",
26+
)
27+
28+
# Read the content of the file and assert it matches the expected content
29+
expected_content = (
30+
"[report]\n"
31+
"include_namespace_packages=True\n"
32+
"skip_covered=True\n"
33+
"[run]\n"
34+
"relative_files=True\n"
35+
"branch=True\n"
36+
)
37+
38+
with open(coverage_rc_path, "r") as file:
39+
file_content = file.read()
40+
41+
self.assertEqual(
42+
file_content,
43+
expected_content,
44+
"COVERAGE_RC file content does not match the expected content",
45+
)
46+
47+
48+
if __name__ == "__main__":
49+
unittest.main()

python/BUILD.bazel

+5
Original file line numberDiff line numberDiff line change
@@ -363,3 +363,8 @@ exports_files([
363363
current_py_toolchain(
364364
name = "current_py_toolchain",
365365
)
366+
367+
toolchain_type(
368+
name = "py_test_toolchain_type",
369+
visibility = ["//visibility:public"],
370+
)

python/private/py_executable.bzl

+19-4
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ load(
6060
load(
6161
":toolchain_types.bzl",
6262
"EXEC_TOOLS_TOOLCHAIN_TYPE",
63+
"PY_TEST_TOOLCHAIN_TYPE",
6364
TOOLCHAIN_TYPE = "TARGET_TOOLCHAIN_TYPE",
6465
)
6566

@@ -254,6 +255,7 @@ def py_executable_base_impl(ctx, *, semantics, is_test, inherited_environment =
254255
inherited_environment = inherited_environment,
255256
semantics = semantics,
256257
output_groups = exec_result.output_groups,
258+
is_test = is_test,
257259
)
258260

259261
def _get_build_info(ctx, cc_toolchain):
@@ -819,7 +821,8 @@ def _create_providers(
819821
inherited_environment,
820822
runtime_details,
821823
output_groups,
822-
semantics):
824+
semantics,
825+
is_test):
823826
"""Creates the providers an executable should return.
824827
825828
Args:
@@ -851,21 +854,32 @@ def _create_providers(
851854
Returns:
852855
A list of modern providers.
853856
"""
857+
858+
default_runfiles = runfiles_details.default_runfiles
859+
extra_test_env = {}
860+
861+
if is_test:
862+
py_test_toolchain = ctx.exec_groups["test"].toolchains[PY_TEST_TOOLCHAIN_TYPE]
863+
if py_test_toolchain:
864+
coverage_rc = py_test_toolchain.py_test_info.coverage_rc
865+
extra_test_env = {"COVERAGE_RC": coverage_rc.files.to_list()[0].path}
866+
default_runfiles = default_runfiles.merge(ctx.runfiles(files = coverage_rc.files.to_list()))
867+
854868
providers = [
855869
DefaultInfo(
856870
executable = executable,
857871
files = default_outputs,
858872
default_runfiles = _py_builtins.make_runfiles_respect_legacy_external_runfiles(
859873
ctx,
860-
runfiles_details.default_runfiles,
874+
default_runfiles,
861875
),
862876
data_runfiles = _py_builtins.make_runfiles_respect_legacy_external_runfiles(
863877
ctx,
864878
runfiles_details.data_runfiles,
865879
),
866880
),
867881
create_instrumented_files_info(ctx),
868-
_create_run_environment_info(ctx, inherited_environment),
882+
_create_run_environment_info(ctx, inherited_environment, extra_test_env),
869883
PyExecutableInfo(
870884
main = main_py,
871885
runfiles_without_exe = runfiles_details.runfiles_without_exe,
@@ -937,7 +951,7 @@ def _create_providers(
937951
providers.extend(extra_providers)
938952
return providers
939953

940-
def _create_run_environment_info(ctx, inherited_environment):
954+
def _create_run_environment_info(ctx, inherited_environment, extra_test_env):
941955
expanded_env = {}
942956
for key, value in ctx.attr.env.items():
943957
expanded_env[key] = _py_builtins.expand_location_and_make_variables(
@@ -946,6 +960,7 @@ def _create_run_environment_info(ctx, inherited_environment):
946960
expression = value,
947961
targets = ctx.attr.data,
948962
)
963+
expanded_env.update(extra_test_env)
949964
return RunEnvironmentInfo(
950965
environment = expanded_env,
951966
inherited_environment = inherited_environment,

python/private/py_test_rule_bazel.bzl

+4
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"""Rule implementation of py_test for Bazel."""
1515

1616
load("@bazel_skylib//lib:dicts.bzl", "dicts")
17+
load("//python/private:toolchain_types.bzl", "PY_TEST_TOOLCHAIN_TYPE")
1718
load(":attributes.bzl", "AGNOSTIC_TEST_ATTRS")
1819
load(":common.bzl", "maybe_add_test_execution_info")
1920
load(
@@ -52,4 +53,7 @@ py_test = create_executable_rule(
5253
implementation = _py_test_impl,
5354
attrs = dicts.add(AGNOSTIC_TEST_ATTRS, _BAZEL_PY_TEST_ATTRS),
5455
test = True,
56+
exec_groups = {
57+
"test": exec_group(toolchains = [config_common.toolchain_type(PY_TEST_TOOLCHAIN_TYPE, mandatory = False)]),
58+
},
5559
)

python/private/py_test_toolchain.bzl

+77
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# Copyright 2024 The Bazel Authors. All rights reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""
16+
Simple toolchain which overrides env and exec requirements.
17+
"""
18+
19+
PytestProvider = provider(
20+
fields = [
21+
"coverage_rc",
22+
],
23+
)
24+
25+
def _py_test_toolchain_impl(ctx):
26+
return [
27+
platform_common.ToolchainInfo(
28+
py_test_info = PytestProvider(
29+
coverage_rc = ctx.attr.coverage_rc,
30+
),
31+
),
32+
]
33+
34+
py_test_toolchain = rule(
35+
implementation = _py_test_toolchain_impl,
36+
attrs = {
37+
"coverage_rc": attr.label(
38+
allow_single_file = True,
39+
),
40+
},
41+
)
42+
_TOOLCHAIN_TEMPLATE = """
43+
load("@rules_python//python/private:py_test_toolchain.bzl", "py_test_toolchain")
44+
py_test_toolchain(
45+
name = "{name}_toolchain",
46+
coverage_rc = "{coverage_rc}",
47+
)
48+
49+
toolchain(
50+
name = "{name}",
51+
target_compatible_with = [],
52+
exec_compatible_with = [],
53+
toolchain = "{name}_toolchain",
54+
toolchain_type = "{toolchain_type}",
55+
)
56+
"""
57+
58+
def _toolchains_repo_impl(repository_ctx):
59+
build_content = _TOOLCHAIN_TEMPLATE.format(
60+
name = repository_ctx.name,
61+
toolchain_type = repository_ctx.attr.toolchain_type,
62+
coverage_rc = repository_ctx.attr.coverage_rc,
63+
)
64+
repository_ctx.file("BUILD.bazel", build_content)
65+
66+
py_test_toolchain_repo = repository_rule(
67+
_toolchains_repo_impl,
68+
doc = "Generates a toolchain hub repository",
69+
attrs = {
70+
"toolchain_type": attr.string(doc = "Toolchain type", mandatory = True),
71+
"coverage_rc": attr.label(
72+
allow_single_file = True,
73+
doc = "The coverage rc file",
74+
mandatory = True,
75+
),
76+
},
77+
)

python/private/py_toolchain_suite.bzl

+12
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,13 @@
1515
"""Create the toolchain defs in a BUILD.bazel file."""
1616

1717
load("@bazel_skylib//lib:selects.bzl", "selects")
18+
load(":py_test_toolchain.bzl", "py_test_toolchain_repo")
1819
load(":text_util.bzl", "render")
1920
load(
2021
":toolchain_types.bzl",
2122
"EXEC_TOOLS_TOOLCHAIN_TYPE",
2223
"PY_CC_TOOLCHAIN_TYPE",
24+
"PY_TEST_TOOLCHAIN_TYPE",
2325
"TARGET_TOOLCHAIN_TYPE",
2426
)
2527

@@ -177,3 +179,13 @@ def define_local_toolchain_suites(name, version_aware_repo_names, version_unawar
177179
target_settings = [],
178180
target_compatible_with = ["@{}//:os".format(repo)],
179181
)
182+
183+
def register_py_test_toolchain(name, coverage_rc, register_toolchains = True):
184+
# Need to create a repository rule for this to work.
185+
py_test_toolchain_repo(
186+
name = "{}_py_test_toolchain".format(name),
187+
coverage_rc = coverage_rc,
188+
toolchain_type = str(PY_TEST_TOOLCHAIN_TYPE),
189+
)
190+
if register_toolchains:
191+
native.toolchain(name = "{}_py_test_toolchain".format(name))

python/private/python.bzl

+21
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
load("@bazel_features//:features.bzl", "bazel_features")
1818
load("//python:versions.bzl", "DEFAULT_RELEASE_BASE_URL", "PLATFORMS", "TOOL_VERSIONS")
19+
load("//python/private:py_toolchain_suite.bzl", "register_py_test_toolchain")
1920
load(":auth.bzl", "AUTH_ATTRS")
2021
load(":full_version.bzl", "full_version")
2122
load(":python_register_toolchains.bzl", "python_register_toolchains")
@@ -80,6 +81,12 @@ def parse_modules(*, module_ctx, _fail = fail):
8081

8182
seen_versions = {}
8283
for mod in module_ctx.modules:
84+
for tag in mod.tags.converage:
85+
register_py_test_toolchain(
86+
name = tag.name,
87+
coverage_rc = tag.coveragerc,
88+
register_toolchains = False,
89+
)
8390
module_toolchain_versions = []
8491
toolchain_attr_structs = _create_toolchain_attr_structs(
8592
mod = mod,
@@ -850,6 +857,19 @@ The coverage tool to be used for a particular Python interpreter. This can overr
850857
),
851858
},
852859
)
860+
_converage = tag_class(
861+
doc = """Tag class used to register Python toolchains.""",
862+
attrs = {
863+
"name": attr.string(
864+
mandatory = True,
865+
doc = "Whether or not to configure the default coverage tool for the toolchains.",
866+
),
867+
"coveragerc": attr.label(
868+
doc = """ """,
869+
mandatory = True,
870+
),
871+
},
872+
)
853873

854874
python = module_extension(
855875
doc = """Bzlmod extension that is used to register Python toolchains.
@@ -860,6 +880,7 @@ python = module_extension(
860880
"single_version_override": _single_version_override,
861881
"single_version_platform_override": _single_version_platform_override,
862882
"toolchain": _toolchain,
883+
"converage": _converage,
863884
},
864885
**_get_bazel_version_specific_kwargs()
865886
)

python/private/stage2_bootstrap_template.py

+11-5
Original file line numberDiff line numberDiff line change
@@ -345,13 +345,19 @@ def _maybe_collect_coverage(enable):
345345
unique_id = uuid.uuid4()
346346

347347
# We need for coveragepy to use relative paths. This can only be configured
348-
rcfile_name = os.path.join(coverage_dir, ".coveragerc_{}".format(unique_id))
349-
with open(rcfile_name, "w") as rcfile:
350-
rcfile.write(
351-
"""[run]
348+
if os.environ.get("COVERAGE_RC"):
349+
rcfile_name = os.path.abspath(os.environ["COVERAGE_RC"])
350+
assert (
351+
os.path.exists(rcfile_name) == True
352+
), f"Coverage rc {rcfile_name} file does not exist"
353+
else:
354+
rcfile_name = os.path.join(coverage_dir, ".coveragerc_{}".format(unique_id))
355+
with open(rcfile_name, "w") as rcfile:
356+
rcfile.write(
357+
"""[run]
352358
relative_files = True
353359
"""
354-
)
360+
)
355361
try:
356362
cov = coverage.Coverage(
357363
config_file=rcfile_name,

python/private/toolchain_types.bzl

+1
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,4 @@ implementation of the toolchain.
2121
TARGET_TOOLCHAIN_TYPE = Label("//python:toolchain_type")
2222
EXEC_TOOLS_TOOLCHAIN_TYPE = Label("//python:exec_tools_toolchain_type")
2323
PY_CC_TOOLCHAIN_TYPE = Label("//python/cc:toolchain_type")
24+
PY_TEST_TOOLCHAIN_TYPE = Label("//python:py_test_toolchain_type")

0 commit comments

Comments
 (0)