Skip to content

Commit 2667f78

Browse files
committed
feat(toolchain): Python testing toolchain
Inspired by https://github.com/trybka/scraps/blob/master/cc_test.md This PR extends Test Runner enviroment to provide a coveragerc enviroment variable COVERAGE_RC, allowing user to provide coverage resource in what ever format
1 parent 188598a commit 2667f78

14 files changed

+325
-14
lines changed

examples/bzlmod/.coveragerc

Lines changed: 6 additions & 0 deletions
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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ load("@rules_python//python:py_binary.bzl", "py_binary")
1313
load("@rules_python//python:py_library.bzl", "py_library")
1414
load("@rules_python//python:py_test.bzl", "py_test")
1515

16+
exports_files([".coveragerc"])
17+
1618
# This stanza calls a rule that generates targets for managing pip dependencies
1719
# with pip-compile for a particular python version.
1820
compile_pip_requirements_3_10(

examples/bzlmod/MODULE.bazel

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,14 @@ python.single_version_platform_override(
104104
# rules based on the `python_version` arg values.
105105
use_repo(python, "python_3_10", "python_3_9", "python_versions", "pythons_hub")
106106

107+
python_test = use_extension("@rules_python//python/extensions:python_test.bzl", "python_test")
108+
python_test.configure(
109+
coveragerc = ".coveragerc",
110+
)
111+
use_repo(python_test, "py_test_toolchain")
112+
113+
register_toolchains("@py_test_toolchain//:all")
114+
107115
# EXPERIMENTAL: This is experimental and may be removed without notice
108116
uv = use_extension("@rules_python//python/uv:extensions.bzl", "uv")
109117
uv.toolchain(uv_version = "0.4.25")

examples/bzlmod/tests/BUILD.bazel

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,11 +49,9 @@ py_test(
4949
)
5050

5151
py_test(
52-
name = "my_lib_3_9_test",
53-
srcs = ["my_lib_test.py"],
54-
main = "my_lib_test.py",
55-
python_version = "3.9",
56-
deps = ["//libs/my_lib"],
52+
name = "coverage_rc_is_set_test",
53+
srcs = ["coverage_rc_is_set_test.py"],
54+
main = "coverage_rc_is_set_test.py",
5755
)
5856

5957
py_test(
Lines changed: 49 additions & 0 deletions
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

Lines changed: 5 additions & 0 deletions
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/extensions/python_test.bzl

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# Copyright 2023 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+
"""Python toolchain module extensions for use with bzlmod.
16+
17+
::::{topic} Basic usage
18+
19+
The simplest way to configure the toolchain with `rules_python` is as follows.
20+
21+
```starlark
22+
python_test = use_extension("@rules_python//python/extensions:python_test.bzl", "python_test")
23+
python_test.configure(
24+
coveragerc = ".coveragerc",
25+
)
26+
use_repo(python_test, "py_test_toolchain")
27+
register_toolchains("@py_test_toolchain//:all")
28+
```
29+
30+
:::{seealso}
31+
For more in-depth documentation see the {obj}`python.toolchain`.
32+
:::
33+
::::
34+
35+
"""
36+
37+
load("//python/private:python_test.bzl", _python_test = "python_test")
38+
39+
python_test = _python_test

python/private/py_executable.bzl

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ load(
6969
load(
7070
":toolchain_types.bzl",
7171
"EXEC_TOOLS_TOOLCHAIN_TYPE",
72+
"PY_TEST_TOOLCHAIN_TYPE",
7273
"TARGET_TOOLCHAIN_TYPE",
7374
TOOLCHAIN_TYPE = "TARGET_TOOLCHAIN_TYPE",
7475
)
@@ -1015,6 +1016,7 @@ def py_executable_base_impl(ctx, *, semantics, is_test, inherited_environment =
10151016
inherited_environment = inherited_environment,
10161017
semantics = semantics,
10171018
output_groups = exec_result.output_groups,
1019+
is_test = is_test,
10181020
)
10191021

10201022
def _get_build_info(ctx, cc_toolchain):
@@ -1580,7 +1582,8 @@ def _create_providers(
15801582
inherited_environment,
15811583
runtime_details,
15821584
output_groups,
1583-
semantics):
1585+
semantics,
1586+
is_test):
15841587
"""Creates the providers an executable should return.
15851588
15861589
Args:
@@ -1614,21 +1617,32 @@ def _create_providers(
16141617
Returns:
16151618
A list of modern providers.
16161619
"""
1620+
1621+
default_runfiles = runfiles_details.default_runfiles
1622+
extra_test_env = {}
1623+
1624+
if is_test:
1625+
py_test_toolchain = ctx.exec_groups["test"].toolchains[PY_TEST_TOOLCHAIN_TYPE]
1626+
if py_test_toolchain:
1627+
coverage_rc = py_test_toolchain.py_test_info.coverage_rc
1628+
extra_test_env = {"COVERAGE_RC": coverage_rc.files.to_list()[0].path}
1629+
default_runfiles = default_runfiles.merge(ctx.runfiles(files = coverage_rc.files.to_list()))
1630+
16171631
providers = [
16181632
DefaultInfo(
16191633
executable = executable,
16201634
files = default_outputs,
16211635
default_runfiles = _py_builtins.make_runfiles_respect_legacy_external_runfiles(
16221636
ctx,
1623-
runfiles_details.default_runfiles,
1637+
default_runfiles,
16241638
),
16251639
data_runfiles = _py_builtins.make_runfiles_respect_legacy_external_runfiles(
16261640
ctx,
16271641
runfiles_details.data_runfiles,
16281642
),
16291643
),
16301644
create_instrumented_files_info(ctx),
1631-
_create_run_environment_info(ctx, inherited_environment),
1645+
_create_run_environment_info(ctx, inherited_environment, extra_test_env),
16321646
PyExecutableInfo(
16331647
main = main_py,
16341648
runfiles_without_exe = runfiles_details.runfiles_without_exe,
@@ -1701,7 +1715,7 @@ def _create_providers(
17011715
providers.extend(extra_providers)
17021716
return providers
17031717

1704-
def _create_run_environment_info(ctx, inherited_environment):
1718+
def _create_run_environment_info(ctx, inherited_environment, extra_test_env):
17051719
expanded_env = {}
17061720
for key, value in ctx.attr.env.items():
17071721
expanded_env[key] = _py_builtins.expand_location_and_make_variables(
@@ -1710,6 +1724,7 @@ def _create_run_environment_info(ctx, inherited_environment):
17101724
expression = value,
17111725
targets = ctx.attr.data,
17121726
)
1727+
expanded_env.update(extra_test_env)
17131728
return RunEnvironmentInfo(
17141729
environment = expanded_env,
17151730
inherited_environment = inherited_environment,

python/private/py_test_rule.bzl

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"""Implementation of py_test rule."""
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

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
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+
load(":text_util.bzl", "render")
20+
load(
21+
":toolchain_types.bzl",
22+
"PY_TEST_TOOLCHAIN_TYPE",
23+
)
24+
25+
PytestProvider = provider(
26+
fields = [
27+
"coverage_rc",
28+
],
29+
)
30+
31+
def _py_test_toolchain_impl(ctx):
32+
return [
33+
platform_common.ToolchainInfo(
34+
py_test_info = PytestProvider(
35+
coverage_rc = ctx.attr.coverage_rc,
36+
),
37+
),
38+
]
39+
40+
py_test_toolchain = rule(
41+
implementation = _py_test_toolchain_impl,
42+
attrs = {
43+
"coverage_rc": attr.label(
44+
allow_single_file = True,
45+
),
46+
},
47+
)
48+
49+
_TOOLCHAIN_TEMPLATE = """
50+
load("@rules_python//python/private:py_test_toolchain.bzl", "py_test_toolchain_macro")
51+
py_test_toolchain_macro(
52+
{kwargs}
53+
)
54+
"""
55+
56+
def py_test_toolchain_macro(*, name, coverage_rc, toolchain_type):
57+
"""
58+
Macro to create a py_test_toolchain rule and a native toolchain rule.
59+
"""
60+
py_test_toolchain(
61+
name = "{}_toolchain".format(name),
62+
coverage_rc = coverage_rc,
63+
)
64+
native.toolchain(
65+
name = name,
66+
target_compatible_with = [],
67+
exec_compatible_with = [],
68+
toolchain = "{}_toolchain".format(name),
69+
toolchain_type = toolchain_type,
70+
)
71+
72+
def _toolchains_repo_impl(repository_ctx):
73+
kwargs = dict(
74+
name = repository_ctx.name,
75+
coverage_rc = str(repository_ctx.attr.coverage_rc),
76+
toolchain_type = repository_ctx.attr.toolchain_type,
77+
)
78+
79+
build_content = _TOOLCHAIN_TEMPLATE.format(
80+
kwargs = render.indent("\n".join([
81+
"{} = {},".format(k, render.str(v))
82+
for k, v in kwargs.items()
83+
])),
84+
)
85+
repository_ctx.file("BUILD.bazel", build_content)
86+
87+
py_test_toolchain_repo = repository_rule(
88+
_toolchains_repo_impl,
89+
doc = "Generates a toolchain hub repository",
90+
attrs = {
91+
"toolchain_type": attr.string(doc = "Toolchain type", mandatory = True),
92+
"coverage_rc": attr.label(
93+
allow_single_file = True,
94+
doc = "The coverage rc file",
95+
mandatory = True,
96+
),
97+
},
98+
)
99+
100+
def register_py_test_toolchain(coverage_rc, register_toolchains = True):
101+
# Need to create a repository rule for this to work.
102+
py_test_toolchain_repo(
103+
name = "py_test_toolchain",
104+
coverage_rc = coverage_rc,
105+
toolchain_type = str(PY_TEST_TOOLCHAIN_TYPE),
106+
)
107+
if register_toolchains:
108+
native.toolchain(name = "py_test_toolchain")

0 commit comments

Comments
 (0)