From 2667f781676acf0e46d1af35534fc5dcc1ab636a Mon Sep 17 00:00:00 2001 From: Elvis Wianda Date: Thu, 19 Sep 2024 01:35:13 -0400 Subject: [PATCH 1/4] 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 --- examples/bzlmod/.coveragerc | 6 + examples/bzlmod/BUILD.bazel | 2 + examples/bzlmod/MODULE.bazel | 8 ++ examples/bzlmod/tests/BUILD.bazel | 8 +- .../bzlmod/tests/coverage_rc_is_set_test.py | 49 ++++++++ python/BUILD.bazel | 5 + python/extensions/python_test.bzl | 39 +++++++ python/private/py_executable.bzl | 23 +++- python/private/py_test_rule.bzl | 4 + python/private/py_test_toolchain.bzl | 108 ++++++++++++++++++ python/private/py_toolchain_suite.bzl | 1 + python/private/python_test.bzl | 69 +++++++++++ python/private/stage2_bootstrap_template.py | 16 ++- python/private/toolchain_types.bzl | 1 + 14 files changed, 325 insertions(+), 14 deletions(-) create mode 100644 examples/bzlmod/.coveragerc create mode 100644 examples/bzlmod/tests/coverage_rc_is_set_test.py create mode 100644 python/extensions/python_test.bzl create mode 100644 python/private/py_test_toolchain.bzl create mode 100644 python/private/python_test.bzl diff --git a/examples/bzlmod/.coveragerc b/examples/bzlmod/.coveragerc new file mode 100644 index 0000000000..5d834b366d --- /dev/null +++ b/examples/bzlmod/.coveragerc @@ -0,0 +1,6 @@ +[report] +include_namespace_packages=True +skip_covered=True +[run] +relative_files=True +branch=True diff --git a/examples/bzlmod/BUILD.bazel b/examples/bzlmod/BUILD.bazel index df07385690..9435bed0b7 100644 --- a/examples/bzlmod/BUILD.bazel +++ b/examples/bzlmod/BUILD.bazel @@ -13,6 +13,8 @@ load("@rules_python//python:py_binary.bzl", "py_binary") load("@rules_python//python:py_library.bzl", "py_library") load("@rules_python//python:py_test.bzl", "py_test") +exports_files([".coveragerc"]) + # This stanza calls a rule that generates targets for managing pip dependencies # with pip-compile for a particular python version. compile_pip_requirements_3_10( diff --git a/examples/bzlmod/MODULE.bazel b/examples/bzlmod/MODULE.bazel index 536e3b2b67..f05f6108ce 100644 --- a/examples/bzlmod/MODULE.bazel +++ b/examples/bzlmod/MODULE.bazel @@ -104,6 +104,14 @@ python.single_version_platform_override( # rules based on the `python_version` arg values. use_repo(python, "python_3_10", "python_3_9", "python_versions", "pythons_hub") +python_test = use_extension("@rules_python//python/extensions:python_test.bzl", "python_test") +python_test.configure( + coveragerc = ".coveragerc", +) +use_repo(python_test, "py_test_toolchain") + +register_toolchains("@py_test_toolchain//:all") + # EXPERIMENTAL: This is experimental and may be removed without notice uv = use_extension("@rules_python//python/uv:extensions.bzl", "uv") uv.toolchain(uv_version = "0.4.25") diff --git a/examples/bzlmod/tests/BUILD.bazel b/examples/bzlmod/tests/BUILD.bazel index 4650fb8788..ee367dbc48 100644 --- a/examples/bzlmod/tests/BUILD.bazel +++ b/examples/bzlmod/tests/BUILD.bazel @@ -49,11 +49,9 @@ py_test( ) py_test( - name = "my_lib_3_9_test", - srcs = ["my_lib_test.py"], - main = "my_lib_test.py", - python_version = "3.9", - deps = ["//libs/my_lib"], + name = "coverage_rc_is_set_test", + srcs = ["coverage_rc_is_set_test.py"], + main = "coverage_rc_is_set_test.py", ) py_test( diff --git a/examples/bzlmod/tests/coverage_rc_is_set_test.py b/examples/bzlmod/tests/coverage_rc_is_set_test.py new file mode 100644 index 0000000000..7b156973b4 --- /dev/null +++ b/examples/bzlmod/tests/coverage_rc_is_set_test.py @@ -0,0 +1,49 @@ +# Copyright 2024 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. +import os +import tempfile +import unittest + + +class TestEnvironmentVariables(unittest.TestCase): + def test_coverage_rc_file_exists(self): + # Assert that the environment variable is set and points to a valid file + coverage_rc_path = os.environ.get("COVERAGE_RC") + self.assertTrue( + os.path.isfile(coverage_rc_path), + "COVERAGE_RC does not point to a valid file", + ) + + # Read the content of the file and assert it matches the expected content + expected_content = ( + "[report]\n" + "include_namespace_packages=True\n" + "skip_covered=True\n" + "[run]\n" + "relative_files=True\n" + "branch=True\n" + ) + + with open(coverage_rc_path, "r") as file: + file_content = file.read() + + self.assertEqual( + file_content, + expected_content, + "COVERAGE_RC file content does not match the expected content", + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/python/BUILD.bazel b/python/BUILD.bazel index b747e2fbc7..b4cfe9d72b 100644 --- a/python/BUILD.bazel +++ b/python/BUILD.bazel @@ -363,3 +363,8 @@ exports_files([ current_py_toolchain( name = "current_py_toolchain", ) + +toolchain_type( + name = "py_test_toolchain_type", + visibility = ["//visibility:public"], +) diff --git a/python/extensions/python_test.bzl b/python/extensions/python_test.bzl new file mode 100644 index 0000000000..1345012bc1 --- /dev/null +++ b/python/extensions/python_test.bzl @@ -0,0 +1,39 @@ +# Copyright 2023 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. + +"""Python toolchain module extensions for use with bzlmod. + +::::{topic} Basic usage + +The simplest way to configure the toolchain with `rules_python` is as follows. + +```starlark +python_test = use_extension("@rules_python//python/extensions:python_test.bzl", "python_test") +python_test.configure( + coveragerc = ".coveragerc", +) +use_repo(python_test, "py_test_toolchain") +register_toolchains("@py_test_toolchain//:all") +``` + +:::{seealso} +For more in-depth documentation see the {obj}`python.toolchain`. +::: +:::: + +""" + +load("//python/private:python_test.bzl", _python_test = "python_test") + +python_test = _python_test diff --git a/python/private/py_executable.bzl b/python/private/py_executable.bzl index da7127e070..3f38b1cb0f 100644 --- a/python/private/py_executable.bzl +++ b/python/private/py_executable.bzl @@ -69,6 +69,7 @@ load( load( ":toolchain_types.bzl", "EXEC_TOOLS_TOOLCHAIN_TYPE", + "PY_TEST_TOOLCHAIN_TYPE", "TARGET_TOOLCHAIN_TYPE", TOOLCHAIN_TYPE = "TARGET_TOOLCHAIN_TYPE", ) @@ -1015,6 +1016,7 @@ def py_executable_base_impl(ctx, *, semantics, is_test, inherited_environment = inherited_environment = inherited_environment, semantics = semantics, output_groups = exec_result.output_groups, + is_test = is_test, ) def _get_build_info(ctx, cc_toolchain): @@ -1580,7 +1582,8 @@ def _create_providers( inherited_environment, runtime_details, output_groups, - semantics): + semantics, + is_test): """Creates the providers an executable should return. Args: @@ -1614,13 +1617,24 @@ def _create_providers( Returns: A list of modern providers. """ + + default_runfiles = runfiles_details.default_runfiles + extra_test_env = {} + + if is_test: + py_test_toolchain = ctx.exec_groups["test"].toolchains[PY_TEST_TOOLCHAIN_TYPE] + if py_test_toolchain: + coverage_rc = py_test_toolchain.py_test_info.coverage_rc + extra_test_env = {"COVERAGE_RC": coverage_rc.files.to_list()[0].path} + default_runfiles = default_runfiles.merge(ctx.runfiles(files = coverage_rc.files.to_list())) + providers = [ DefaultInfo( executable = executable, files = default_outputs, default_runfiles = _py_builtins.make_runfiles_respect_legacy_external_runfiles( ctx, - runfiles_details.default_runfiles, + default_runfiles, ), data_runfiles = _py_builtins.make_runfiles_respect_legacy_external_runfiles( ctx, @@ -1628,7 +1642,7 @@ def _create_providers( ), ), create_instrumented_files_info(ctx), - _create_run_environment_info(ctx, inherited_environment), + _create_run_environment_info(ctx, inherited_environment, extra_test_env), PyExecutableInfo( main = main_py, runfiles_without_exe = runfiles_details.runfiles_without_exe, @@ -1701,7 +1715,7 @@ def _create_providers( providers.extend(extra_providers) return providers -def _create_run_environment_info(ctx, inherited_environment): +def _create_run_environment_info(ctx, inherited_environment, extra_test_env): expanded_env = {} for key, value in ctx.attr.env.items(): expanded_env[key] = _py_builtins.expand_location_and_make_variables( @@ -1710,6 +1724,7 @@ def _create_run_environment_info(ctx, inherited_environment): expression = value, targets = ctx.attr.data, ) + expanded_env.update(extra_test_env) return RunEnvironmentInfo( environment = expanded_env, inherited_environment = inherited_environment, diff --git a/python/private/py_test_rule.bzl b/python/private/py_test_rule.bzl index 63000c7255..dcf55dd44d 100644 --- a/python/private/py_test_rule.bzl +++ b/python/private/py_test_rule.bzl @@ -14,6 +14,7 @@ """Implementation of py_test rule.""" load("@bazel_skylib//lib:dicts.bzl", "dicts") +load("//python/private:toolchain_types.bzl", "PY_TEST_TOOLCHAIN_TYPE") load(":attributes.bzl", "AGNOSTIC_TEST_ATTRS") load(":common.bzl", "maybe_add_test_execution_info") load( @@ -52,4 +53,7 @@ py_test = create_executable_rule( implementation = _py_test_impl, attrs = dicts.add(AGNOSTIC_TEST_ATTRS, _BAZEL_PY_TEST_ATTRS), test = True, + exec_groups = { + "test": exec_group(toolchains = [config_common.toolchain_type(PY_TEST_TOOLCHAIN_TYPE, mandatory = False)]), + }, ) diff --git a/python/private/py_test_toolchain.bzl b/python/private/py_test_toolchain.bzl new file mode 100644 index 0000000000..0515fdace9 --- /dev/null +++ b/python/private/py_test_toolchain.bzl @@ -0,0 +1,108 @@ +# Copyright 2024 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. + +""" +Simple toolchain which overrides env and exec requirements. +""" + +load(":text_util.bzl", "render") +load( + ":toolchain_types.bzl", + "PY_TEST_TOOLCHAIN_TYPE", +) + +PytestProvider = provider( + fields = [ + "coverage_rc", + ], +) + +def _py_test_toolchain_impl(ctx): + return [ + platform_common.ToolchainInfo( + py_test_info = PytestProvider( + coverage_rc = ctx.attr.coverage_rc, + ), + ), + ] + +py_test_toolchain = rule( + implementation = _py_test_toolchain_impl, + attrs = { + "coverage_rc": attr.label( + allow_single_file = True, + ), + }, +) + +_TOOLCHAIN_TEMPLATE = """ +load("@rules_python//python/private:py_test_toolchain.bzl", "py_test_toolchain_macro") +py_test_toolchain_macro( + {kwargs} +) +""" + +def py_test_toolchain_macro(*, name, coverage_rc, toolchain_type): + """ + Macro to create a py_test_toolchain rule and a native toolchain rule. + """ + py_test_toolchain( + name = "{}_toolchain".format(name), + coverage_rc = coverage_rc, + ) + native.toolchain( + name = name, + target_compatible_with = [], + exec_compatible_with = [], + toolchain = "{}_toolchain".format(name), + toolchain_type = toolchain_type, + ) + +def _toolchains_repo_impl(repository_ctx): + kwargs = dict( + name = repository_ctx.name, + coverage_rc = str(repository_ctx.attr.coverage_rc), + toolchain_type = repository_ctx.attr.toolchain_type, + ) + + build_content = _TOOLCHAIN_TEMPLATE.format( + kwargs = render.indent("\n".join([ + "{} = {},".format(k, render.str(v)) + for k, v in kwargs.items() + ])), + ) + repository_ctx.file("BUILD.bazel", build_content) + +py_test_toolchain_repo = repository_rule( + _toolchains_repo_impl, + doc = "Generates a toolchain hub repository", + attrs = { + "toolchain_type": attr.string(doc = "Toolchain type", mandatory = True), + "coverage_rc": attr.label( + allow_single_file = True, + doc = "The coverage rc file", + mandatory = True, + ), + }, +) + +def register_py_test_toolchain(coverage_rc, register_toolchains = True): + # Need to create a repository rule for this to work. + py_test_toolchain_repo( + name = "py_test_toolchain", + coverage_rc = coverage_rc, + toolchain_type = str(PY_TEST_TOOLCHAIN_TYPE), + ) + if register_toolchains: + native.toolchain(name = "py_test_toolchain") diff --git a/python/private/py_toolchain_suite.bzl b/python/private/py_toolchain_suite.bzl index a69be376b4..0c9f4d2fc3 100644 --- a/python/private/py_toolchain_suite.bzl +++ b/python/private/py_toolchain_suite.bzl @@ -20,6 +20,7 @@ load( ":toolchain_types.bzl", "EXEC_TOOLS_TOOLCHAIN_TYPE", "PY_CC_TOOLCHAIN_TYPE", + "PY_TEST_TOOLCHAIN_TYPE", "TARGET_TOOLCHAIN_TYPE", ) diff --git a/python/private/python_test.bzl b/python/private/python_test.bzl new file mode 100644 index 0000000000..8f4b6a9cb4 --- /dev/null +++ b/python/private/python_test.bzl @@ -0,0 +1,69 @@ +# Copyright 2023 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. + +"Python test toolchain module extensions for use with bzlmod." + +load("//python/private:py_test_toolchain.bzl", "register_py_test_toolchain") +load(":text_util.bzl", "render") + +def _python_test_impl(module_ctx): + """Implementation of the `coverage` extension. + + Configure the test toolchain for setting coverage resource file. + + """ + for mod in module_ctx.modules: + for tag in mod.tags.configure: + register_py_test_toolchain( + coverage_rc = tag.coveragerc, + register_toolchains = False, + ) + +configure = tag_class( + doc = """Tag class used to register Python toolchains.""", + attrs = { + # TODO: Add testrunner and potentially coverage_tool + "coveragerc": attr.label( + doc = """Tag class used to register Python toolchains. + +:::{topic} Toolchains in the Root Module + +:::{tip} +In order to use a different name than the above, you can use the following `MODULE.bazel` +syntax: +```starlark +python = use_extension("@rules_python//python/extensions:python.bzl", "python") +python.toolchain( + is_default = True, + python_version = "3.11", +) + +use_repo(python, my_python_name = "python_3_11") +``` + +Then the python interpreter will be available as `my_python_name`. +::: +""", + mandatory = True, + ), + }, +) + +python_test = module_extension( + doc = """Bzlmod extension that is used to register test toolchains. """, + implementation = _python_test_impl, + tag_classes = { + "configure": configure, + }, +) diff --git a/python/private/stage2_bootstrap_template.py b/python/private/stage2_bootstrap_template.py index 1e19a71b64..dc32df1ba6 100644 --- a/python/private/stage2_bootstrap_template.py +++ b/python/private/stage2_bootstrap_template.py @@ -283,13 +283,19 @@ def _maybe_collect_coverage(enable): unique_id = uuid.uuid4() # We need for coveragepy to use relative paths. This can only be configured - rcfile_name = os.path.join(coverage_dir, ".coveragerc_{}".format(unique_id)) - with open(rcfile_name, "w") as rcfile: - rcfile.write( - """[run] + if os.environ.get("COVERAGE_RC"): + rcfile_name = os.path.abspath(os.environ["COVERAGE_RC"]) + assert ( + os.path.exists(rcfile_name) == True + ), f"Coverage rc {rcfile_name} file does not exist" + else: + rcfile_name = os.path.join(coverage_dir, ".coveragerc_{}".format(unique_id)) + with open(rcfile_name, "w") as rcfile: + rcfile.write( + """[run] relative_files = True """ - ) + ) try: cov = coverage.Coverage( config_file=rcfile_name, diff --git a/python/private/toolchain_types.bzl b/python/private/toolchain_types.bzl index ef81bf3bd4..fbdb4f972d 100644 --- a/python/private/toolchain_types.bzl +++ b/python/private/toolchain_types.bzl @@ -21,3 +21,4 @@ implementation of the toolchain. TARGET_TOOLCHAIN_TYPE = Label("//python:toolchain_type") EXEC_TOOLS_TOOLCHAIN_TYPE = Label("//python:exec_tools_toolchain_type") PY_CC_TOOLCHAIN_TYPE = Label("//python/cc:toolchain_type") +PY_TEST_TOOLCHAIN_TYPE = Label("//python:py_test_toolchain_type") From c3f1d7a90f2aee25162d121b0385f61b69c97b5c Mon Sep 17 00:00:00 2001 From: Elvis Wianda Date: Thu, 31 Oct 2024 11:33:56 -0400 Subject: [PATCH 2/4] Add env variable to rule transitions --- .bazelrc | 4 +-- CHANGELOG.md | 3 ++ examples/bzlmod/MODULE.bazel | 5 ++++ examples/bzlmod/tests/BUILD.bazel | 14 ++++++++++ .../bzlmod/tests/coverage_rc_is_set_test.py | 4 ++- python/extensions/python_test.bzl | 6 ---- python/private/py_executable.bzl | 1 + python/private/py_test_toolchain.bzl | 27 ++++++++++++------ python/private/py_toolchain_suite.bzl | 1 - python/private/python_test.bzl | 28 +++++-------------- 10 files changed, 54 insertions(+), 39 deletions(-) diff --git a/.bazelrc b/.bazelrc index ada5c5a0a7..75749c27d8 100644 --- a/.bazelrc +++ b/.bazelrc @@ -4,8 +4,8 @@ # (Note, we cannot use `common --deleted_packages` because the bazel version command doesn't support it) # To update these lines, execute # `bazel run @rules_bazel_integration_test//tools:update_deleted_packages` -build --deleted_packages=examples/build_file_generation,examples/build_file_generation/random_number_generator,examples/bzlmod,examples/bzlmod_build_file_generation,examples/bzlmod_build_file_generation/other_module/other_module/pkg,examples/bzlmod_build_file_generation/runfiles,examples/bzlmod/entry_points,examples/bzlmod/entry_points/tests,examples/bzlmod/libs/my_lib,examples/bzlmod/other_module,examples/bzlmod/other_module/other_module/pkg,examples/bzlmod/patches,examples/bzlmod/py_proto_library,examples/bzlmod/py_proto_library/example.com/another_proto,examples/bzlmod/py_proto_library/example.com/proto,examples/bzlmod/runfiles,examples/bzlmod/tests,examples/bzlmod/tests/other_module,examples/bzlmod/whl_mods,examples/multi_python_versions/libs/my_lib,examples/multi_python_versions/requirements,examples/multi_python_versions/tests,examples/pip_parse,examples/pip_parse_vendored,examples/pip_repository_annotations,examples/py_proto_library,examples/py_proto_library/example.com/another_proto,examples/py_proto_library/example.com/proto,gazelle,gazelle/manifest,gazelle/manifest/generate,gazelle/manifest/hasher,gazelle/manifest/test,gazelle/modules_mapping,gazelle/python,gazelle/pythonconfig,gazelle/python/private,tests/integration/compile_pip_requirements,tests/integration/compile_pip_requirements_test_from_external_repo,tests/integration/custom_commands,tests/integration/ignore_root_user_error,tests/integration/ignore_root_user_error/submodule,tests/integration/local_toolchains,tests/integration/pip_parse,tests/integration/pip_parse/empty,tests/integration/py_cc_toolchain_registered -query --deleted_packages=examples/build_file_generation,examples/build_file_generation/random_number_generator,examples/bzlmod,examples/bzlmod_build_file_generation,examples/bzlmod_build_file_generation/other_module/other_module/pkg,examples/bzlmod_build_file_generation/runfiles,examples/bzlmod/entry_points,examples/bzlmod/entry_points/tests,examples/bzlmod/libs/my_lib,examples/bzlmod/other_module,examples/bzlmod/other_module/other_module/pkg,examples/bzlmod/patches,examples/bzlmod/py_proto_library,examples/bzlmod/py_proto_library/example.com/another_proto,examples/bzlmod/py_proto_library/example.com/proto,examples/bzlmod/runfiles,examples/bzlmod/tests,examples/bzlmod/tests/other_module,examples/bzlmod/whl_mods,examples/multi_python_versions/libs/my_lib,examples/multi_python_versions/requirements,examples/multi_python_versions/tests,examples/pip_parse,examples/pip_parse_vendored,examples/pip_repository_annotations,examples/py_proto_library,examples/py_proto_library/example.com/another_proto,examples/py_proto_library/example.com/proto,gazelle,gazelle/manifest,gazelle/manifest/generate,gazelle/manifest/hasher,gazelle/manifest/test,gazelle/modules_mapping,gazelle/python,gazelle/pythonconfig,gazelle/python/private,tests/integration/compile_pip_requirements,tests/integration/compile_pip_requirements_test_from_external_repo,tests/integration/custom_commands,tests/integration/ignore_root_user_error,tests/integration/ignore_root_user_error/submodule,tests/integration/local_toolchains,tests/integration/pip_parse,tests/integration/pip_parse/empty,tests/integration/py_cc_toolchain_registered +build --deleted_packages=examples/build_file_generation,examples/build_file_generation/random_number_generator,examples/bzlmod,examples/bzlmod/entry_points,examples/bzlmod/entry_points/tests,examples/bzlmod/libs/my_lib,examples/bzlmod/other_module,examples/bzlmod/other_module/other_module/pkg,examples/bzlmod/patches,examples/bzlmod/py_proto_library,examples/bzlmod/py_proto_library/example.com/another_proto,examples/bzlmod/py_proto_library/example.com/proto,examples/bzlmod/runfiles,examples/bzlmod/tests,examples/bzlmod/tests/other_module,examples/bzlmod/whl_mods,examples/bzlmod_build_file_generation,examples/bzlmod_build_file_generation/other_module/other_module/pkg,examples/bzlmod_build_file_generation/runfiles,examples/multi_python_versions/libs/my_lib,examples/multi_python_versions/requirements,examples/multi_python_versions/tests,examples/pip_parse,examples/pip_parse_vendored,examples/pip_repository_annotations,examples/py_proto_library,examples/py_proto_library/example.com/another_proto,examples/py_proto_library/example.com/proto,gazelle,gazelle/manifest,gazelle/manifest/generate,gazelle/manifest/hasher,gazelle/manifest/test,gazelle/modules_mapping,gazelle/python,gazelle/python/private,gazelle/pythonconfig,tests/integration/compile_pip_requirements,tests/integration/compile_pip_requirements_test_from_external_repo,tests/integration/custom_commands,tests/integration/ignore_root_user_error,tests/integration/ignore_root_user_error/submodule,tests/integration/local_toolchains,tests/integration/pip_parse,tests/integration/pip_parse/empty,tests/integration/py_cc_toolchain_registered +query --deleted_packages=examples/build_file_generation,examples/build_file_generation/random_number_generator,examples/bzlmod,examples/bzlmod/entry_points,examples/bzlmod/entry_points/tests,examples/bzlmod/libs/my_lib,examples/bzlmod/other_module,examples/bzlmod/other_module/other_module/pkg,examples/bzlmod/patches,examples/bzlmod/py_proto_library,examples/bzlmod/py_proto_library/example.com/another_proto,examples/bzlmod/py_proto_library/example.com/proto,examples/bzlmod/runfiles,examples/bzlmod/tests,examples/bzlmod/tests/other_module,examples/bzlmod/whl_mods,examples/bzlmod_build_file_generation,examples/bzlmod_build_file_generation/other_module/other_module/pkg,examples/bzlmod_build_file_generation/runfiles,examples/multi_python_versions/libs/my_lib,examples/multi_python_versions/requirements,examples/multi_python_versions/tests,examples/pip_parse,examples/pip_parse_vendored,examples/pip_repository_annotations,examples/py_proto_library,examples/py_proto_library/example.com/another_proto,examples/py_proto_library/example.com/proto,gazelle,gazelle/manifest,gazelle/manifest/generate,gazelle/manifest/hasher,gazelle/manifest/test,gazelle/modules_mapping,gazelle/python,gazelle/python/private,gazelle/pythonconfig,tests/integration/compile_pip_requirements,tests/integration/compile_pip_requirements_test_from_external_repo,tests/integration/custom_commands,tests/integration/ignore_root_user_error,tests/integration/ignore_root_user_error/submodule,tests/integration/local_toolchains,tests/integration/pip_parse,tests/integration/pip_parse/empty,tests/integration/py_cc_toolchain_registered test --test_output=errors diff --git a/CHANGELOG.md b/CHANGELOG.md index 00624db01f..10ce1aa9e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -363,6 +363,9 @@ Other changes: {#v0-38-0-added} ### Added +* (toolchain) Using testing toolchain to configure py_test coverage. + This opens the potential to configure differnt test runners. + ([#2246](https://github.com/bazelbuild/rules_python/pull/2246)). * (publish) The requirements file for the `twine` publishing rules have been updated to have a new convention: `requirements_darwin.txt`, `requirements_linux.txt`, `requirements_windows.txt` for each respective OS diff --git a/examples/bzlmod/MODULE.bazel b/examples/bzlmod/MODULE.bazel index f05f6108ce..fa58ad00ab 100644 --- a/examples/bzlmod/MODULE.bazel +++ b/examples/bzlmod/MODULE.bazel @@ -7,6 +7,11 @@ module( bazel_dep(name = "bazel_skylib", version = "1.7.1") bazel_dep(name = "platforms", version = "0.0.4") bazel_dep(name = "rules_python", version = "0.0.0") +bazel_dep(name = "bazel_features", version = "1.9.1") + +version = use_extension("@bazel_features//private:extensions.bzl", "version_extension") +use_repo(version, "bazel_features_globals", "bazel_features_version") + local_path_override( module_name = "rules_python", path = "../..", diff --git a/examples/bzlmod/tests/BUILD.bazel b/examples/bzlmod/tests/BUILD.bazel index ee367dbc48..dddab6540d 100644 --- a/examples/bzlmod/tests/BUILD.bazel +++ b/examples/bzlmod/tests/BUILD.bazel @@ -54,6 +54,20 @@ py_test( main = "coverage_rc_is_set_test.py", ) +py_test( + name = "coverage_rc_is_set_3_9_test", + srcs = ["coverage_rc_is_set_test.py"], + main = "coverage_rc_is_set_test.py", + python_version = "3.9", +) + +py_test( + name = "coverage_rc_is_set_3_11_test", + srcs = ["coverage_rc_is_set_test.py"], + main = "coverage_rc_is_set_test.py", + python_version = "3.11", +) + py_test( name = "my_lib_3_10_test", srcs = ["my_lib_test.py"], diff --git a/examples/bzlmod/tests/coverage_rc_is_set_test.py b/examples/bzlmod/tests/coverage_rc_is_set_test.py index 7b156973b4..6bb0fde1e9 100644 --- a/examples/bzlmod/tests/coverage_rc_is_set_test.py +++ b/examples/bzlmod/tests/coverage_rc_is_set_test.py @@ -20,9 +20,11 @@ class TestEnvironmentVariables(unittest.TestCase): def test_coverage_rc_file_exists(self): # Assert that the environment variable is set and points to a valid file coverage_rc_path = os.environ.get("COVERAGE_RC") + if coverage_rc_path: + coverage_rc_path = os.path.abspath(coverage_rc_path) self.assertTrue( os.path.isfile(coverage_rc_path), - "COVERAGE_RC does not point to a valid file", + f"COVERAGE_RC does not point to a valid file, {coverage_rc_path}", ) # Read the content of the file and assert it matches the expected content diff --git a/python/extensions/python_test.bzl b/python/extensions/python_test.bzl index 1345012bc1..c437b5e431 100644 --- a/python/extensions/python_test.bzl +++ b/python/extensions/python_test.bzl @@ -26,12 +26,6 @@ python_test.configure( use_repo(python_test, "py_test_toolchain") register_toolchains("@py_test_toolchain//:all") ``` - -:::{seealso} -For more in-depth documentation see the {obj}`python.toolchain`. -::: -:::: - """ load("//python/private:python_test.bzl", _python_test = "python_test") diff --git a/python/private/py_executable.bzl b/python/private/py_executable.bzl index 3f38b1cb0f..ccd83a04cd 100644 --- a/python/private/py_executable.bzl +++ b/python/private/py_executable.bzl @@ -1613,6 +1613,7 @@ def _create_providers( runtime_details: struct of runtime information; see _get_runtime_details() output_groups: dict[str, depset[File]]; used to create OutputGroupInfo semantics: BinarySemantics struct; see create_binary_semantics() + is_test: bool; True if the rule is a test rule, Returns: A list of modern providers. diff --git a/python/private/py_test_toolchain.bzl b/python/private/py_test_toolchain.bzl index 0515fdace9..d8bc80ae6d 100644 --- a/python/private/py_test_toolchain.bzl +++ b/python/private/py_test_toolchain.bzl @@ -22,7 +22,8 @@ load( "PY_TEST_TOOLCHAIN_TYPE", ) -PytestProvider = provider( +PyTestProviderInfo = provider( + doc = "Information about the pytest toolchain", fields = [ "coverage_rc", ], @@ -31,7 +32,7 @@ PytestProvider = provider( def _py_test_toolchain_impl(ctx): return [ platform_common.ToolchainInfo( - py_test_info = PytestProvider( + py_test_info = PyTestProviderInfo( coverage_rc = ctx.attr.coverage_rc, ), ), @@ -56,6 +57,10 @@ py_test_toolchain_macro( def py_test_toolchain_macro(*, name, coverage_rc, toolchain_type): """ Macro to create a py_test_toolchain rule and a native toolchain rule. + + name: The name of the toolchain. + coverage_rc: The coverage rc file. + toolchain_type: The toolchain type. """ py_test_toolchain( name = "{}_toolchain".format(name), @@ -73,7 +78,7 @@ def _toolchains_repo_impl(repository_ctx): kwargs = dict( name = repository_ctx.name, coverage_rc = str(repository_ctx.attr.coverage_rc), - toolchain_type = repository_ctx.attr.toolchain_type, + toolchain_type = str(repository_ctx.attr.toolchain_type), ) build_content = _TOOLCHAIN_TEMPLATE.format( @@ -88,21 +93,27 @@ py_test_toolchain_repo = repository_rule( _toolchains_repo_impl, doc = "Generates a toolchain hub repository", attrs = { - "toolchain_type": attr.string(doc = "Toolchain type", mandatory = True), "coverage_rc": attr.label( allow_single_file = True, doc = "The coverage rc file", mandatory = True, ), + "toolchain_type": attr.label(doc = "Toolchain type", mandatory = True), }, ) -def register_py_test_toolchain(coverage_rc, register_toolchains = True): - # Need to create a repository rule for this to work. +def register_py_test_toolchain(name, coverage_rc, register_toolchains = True): + """ Register the py_test_toolchain and native toolchain rules. + + name: The name of the toolchain. + coverage_rc: The coverage rc file. + register_toolchains: Whether to register the toolchains. + + """ py_test_toolchain_repo( - name = "py_test_toolchain", + name = name, coverage_rc = coverage_rc, - toolchain_type = str(PY_TEST_TOOLCHAIN_TYPE), + toolchain_type = PY_TEST_TOOLCHAIN_TYPE, ) if register_toolchains: native.toolchain(name = "py_test_toolchain") diff --git a/python/private/py_toolchain_suite.bzl b/python/private/py_toolchain_suite.bzl index 0c9f4d2fc3..a69be376b4 100644 --- a/python/private/py_toolchain_suite.bzl +++ b/python/private/py_toolchain_suite.bzl @@ -20,7 +20,6 @@ load( ":toolchain_types.bzl", "EXEC_TOOLS_TOOLCHAIN_TYPE", "PY_CC_TOOLCHAIN_TYPE", - "PY_TEST_TOOLCHAIN_TYPE", "TARGET_TOOLCHAIN_TYPE", ) diff --git a/python/private/python_test.bzl b/python/private/python_test.bzl index 8f4b6a9cb4..3371b55a4b 100644 --- a/python/private/python_test.bzl +++ b/python/private/python_test.bzl @@ -14,8 +14,8 @@ "Python test toolchain module extensions for use with bzlmod." +load("@bazel_features//:features.bzl", "bazel_features") load("//python/private:py_test_toolchain.bzl", "register_py_test_toolchain") -load(":text_util.bzl", "render") def _python_test_impl(module_ctx): """Implementation of the `coverage` extension. @@ -26,35 +26,21 @@ def _python_test_impl(module_ctx): for mod in module_ctx.modules: for tag in mod.tags.configure: register_py_test_toolchain( + name = "py_test_toolchain", coverage_rc = tag.coveragerc, register_toolchains = False, ) + if bazel_features.external_deps.extension_metadata_has_reproducible: + return module_ctx.extension_metadata(reproducible = True) + else: + return None configure = tag_class( doc = """Tag class used to register Python toolchains.""", attrs = { # TODO: Add testrunner and potentially coverage_tool "coveragerc": attr.label( - doc = """Tag class used to register Python toolchains. - -:::{topic} Toolchains in the Root Module - -:::{tip} -In order to use a different name than the above, you can use the following `MODULE.bazel` -syntax: -```starlark -python = use_extension("@rules_python//python/extensions:python.bzl", "python") -python.toolchain( - is_default = True, - python_version = "3.11", -) - -use_repo(python, my_python_name = "python_3_11") -``` - -Then the python interpreter will be available as `my_python_name`. -::: -""", + doc = """Tag class used to register Python toolchains.""", mandatory = True, ), }, From c111829565cdf988199cf4c91a1844a8d7c71bc4 Mon Sep 17 00:00:00 2001 From: Elvis Wianda Date: Tue, 5 Nov 2024 13:43:00 -0500 Subject: [PATCH 3/4] Move toolchain logic into get runtime details --- python/private/py_executable.bzl | 34 ++++++++++----------- python/private/stage2_bootstrap_template.py | 2 +- 2 files changed, 17 insertions(+), 19 deletions(-) diff --git a/python/private/py_executable.bzl b/python/private/py_executable.bzl index ccd83a04cd..6036ae88f4 100644 --- a/python/private/py_executable.bzl +++ b/python/private/py_executable.bzl @@ -940,7 +940,7 @@ def py_executable_base_impl(ctx, *, semantics, is_test, inherited_environment = imports = collect_imports(ctx, semantics) - runtime_details = _get_runtime_details(ctx, semantics) + runtime_details = _get_runtime_details(ctx, semantics, is_test) if ctx.configuration.coverage_enabled: extra_deps = semantics.get_coverage_deps(ctx, runtime_details) else: @@ -1016,7 +1016,6 @@ def py_executable_base_impl(ctx, *, semantics, is_test, inherited_environment = inherited_environment = inherited_environment, semantics = semantics, output_groups = exec_result.output_groups, - is_test = is_test, ) def _get_build_info(ctx, cc_toolchain): @@ -1041,7 +1040,7 @@ def _declare_executable_file(ctx): return executable -def _get_runtime_details(ctx, semantics): +def _get_runtime_details(ctx, semantics, is_test): """Gets various information about the Python runtime to use. While most information comes from the toolchain, various legacy and @@ -1050,6 +1049,7 @@ def _get_runtime_details(ctx, semantics): Args: ctx: Rule ctx semantics: A `BinarySemantics` struct; see `create_binary_semantics_struct` + is_test: bool; True if the rule is a test rule (has `test=True`), False if not Returns: A struct; see inline-field comments of the return value for details. @@ -1078,6 +1078,7 @@ def _get_runtime_details(ctx, semantics): if not effective_runtime: fail("Unable to find Python runtime") + extra_test_env = {} if effective_runtime: direct = [] # List of files transitive = [] # List of depsets @@ -1090,6 +1091,12 @@ def _get_runtime_details(ctx, semantics): direct.append(effective_runtime.coverage_tool) if effective_runtime.coverage_files: transitive.append(effective_runtime.coverage_files) + if is_test: + py_test_toolchain = ctx.exec_groups["test"].toolchains[PY_TEST_TOOLCHAIN_TYPE] + if py_test_toolchain: + coverage_rc = py_test_toolchain.py_test_info.coverage_rc + extra_test_env = {"COVERAGE_RC": coverage_rc.files.to_list()[0].short_path} + direct.extend(coverage_rc.files.to_list()) runtime_files = depset(direct = direct, transitive = transitive) else: runtime_files = depset() @@ -1121,6 +1128,9 @@ def _get_runtime_details(ctx, semantics): # be included. For in-build runtimes, this shold include the interpreter # and any supporting files. runfiles = ctx.runfiles(transitive_files = runtime_files), + # extra_test_env: dict[str, str]; Additional environment variables to + # set when running the test. + extra_test_env = extra_test_env, ) def _maybe_get_runtime_from_ctx(ctx): @@ -1582,8 +1592,7 @@ def _create_providers( inherited_environment, runtime_details, output_groups, - semantics, - is_test): + semantics): """Creates the providers an executable should return. Args: @@ -1613,29 +1622,18 @@ def _create_providers( runtime_details: struct of runtime information; see _get_runtime_details() output_groups: dict[str, depset[File]]; used to create OutputGroupInfo semantics: BinarySemantics struct; see create_binary_semantics() - is_test: bool; True if the rule is a test rule, Returns: A list of modern providers. """ - default_runfiles = runfiles_details.default_runfiles - extra_test_env = {} - - if is_test: - py_test_toolchain = ctx.exec_groups["test"].toolchains[PY_TEST_TOOLCHAIN_TYPE] - if py_test_toolchain: - coverage_rc = py_test_toolchain.py_test_info.coverage_rc - extra_test_env = {"COVERAGE_RC": coverage_rc.files.to_list()[0].path} - default_runfiles = default_runfiles.merge(ctx.runfiles(files = coverage_rc.files.to_list())) - providers = [ DefaultInfo( executable = executable, files = default_outputs, default_runfiles = _py_builtins.make_runfiles_respect_legacy_external_runfiles( ctx, - default_runfiles, + runfiles_details.default_runfiles, ), data_runfiles = _py_builtins.make_runfiles_respect_legacy_external_runfiles( ctx, @@ -1643,7 +1641,7 @@ def _create_providers( ), ), create_instrumented_files_info(ctx), - _create_run_environment_info(ctx, inherited_environment, extra_test_env), + _create_run_environment_info(ctx, inherited_environment, runtime_details.extra_test_env), PyExecutableInfo( main = main_py, runfiles_without_exe = runfiles_details.runfiles_without_exe, diff --git a/python/private/stage2_bootstrap_template.py b/python/private/stage2_bootstrap_template.py index dc32df1ba6..e4dbcb62b0 100644 --- a/python/private/stage2_bootstrap_template.py +++ b/python/private/stage2_bootstrap_template.py @@ -284,7 +284,7 @@ def _maybe_collect_coverage(enable): # We need for coveragepy to use relative paths. This can only be configured if os.environ.get("COVERAGE_RC"): - rcfile_name = os.path.abspath(os.environ["COVERAGE_RC"]) + rcfile_name = (os.environ["COVERAGE_RC"]) assert ( os.path.exists(rcfile_name) == True ), f"Coverage rc {rcfile_name} file does not exist" From 214972f58108362935c7f695a98c2bee1775f7a4 Mon Sep 17 00:00:00 2001 From: Elvis Wianda Date: Sat, 14 Dec 2024 14:07:34 -0500 Subject: [PATCH 4/4] Factor out Default and RuntimeInfo providers Create partially-bound function for configuring py_test --- CHANGELOG.md | 7 ++-- python/private/py_binary_rule.bzl | 19 +++++++++- python/private/py_executable.bzl | 49 ++++++++++--------------- python/private/py_test_rule.bzl | 29 +++++++++++++-- python/private/py_test_toolchain.bzl | 53 ++++++++++++++++++++++++---- 5 files changed, 113 insertions(+), 44 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 10ce1aa9e1..a3691ac186 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -66,7 +66,9 @@ Unreleased changes template. {#v0-0-0-added} ### Added -* Nothing added. +* (toolchain) Using testing toolchain to configure py_test coverage. + This opens the potential to configure differnt test runners. + ([#2246](https://github.com/bazelbuild/rules_python/pull/2246)). {#v0-0-0-removed} ### Removed @@ -363,9 +365,6 @@ Other changes: {#v0-38-0-added} ### Added -* (toolchain) Using testing toolchain to configure py_test coverage. - This opens the potential to configure differnt test runners. - ([#2246](https://github.com/bazelbuild/rules_python/pull/2246)). * (publish) The requirements file for the `twine` publishing rules have been updated to have a new convention: `requirements_darwin.txt`, `requirements_linux.txt`, `requirements_windows.txt` for each respective OS diff --git a/python/private/py_binary_rule.bzl b/python/private/py_binary_rule.bzl index f1c8eb1325..ce5bc6172b 100644 --- a/python/private/py_binary_rule.bzl +++ b/python/private/py_binary_rule.bzl @@ -39,11 +39,28 @@ _PY_TEST_ATTRS = { } def _py_binary_impl(ctx): - return py_executable_impl( + providers, binary_info, environment_info = py_executable_impl( ctx = ctx, is_test = False, inherited_environment = [], ) + providers.extend( + [ + # We construct DefaultInfo and RunEnvironmentInfo here, as other py_binary-like + # rules (py_test) need a different DefaultInfo and RunEnvironmentInfo. + DefaultInfo( + executable = binary_info.executable, + files = binary_info.files, + default_runfiles = binary_info.default_runfiles, + data_runfiles = binary_info.data_runfiles, + ), + RunEnvironmentInfo( + environment = environment_info.environment, + inherited_environment = environment_info.inherited_environment, + ), + ], + ) + return providers py_binary = create_executable_rule( implementation = _py_binary_impl, diff --git a/python/private/py_executable.bzl b/python/private/py_executable.bzl index 6036ae88f4..9b7dff8af9 100644 --- a/python/private/py_executable.bzl +++ b/python/private/py_executable.bzl @@ -940,7 +940,7 @@ def py_executable_base_impl(ctx, *, semantics, is_test, inherited_environment = imports = collect_imports(ctx, semantics) - runtime_details = _get_runtime_details(ctx, semantics, is_test) + runtime_details = _get_runtime_details(ctx, semantics) if ctx.configuration.coverage_enabled: extra_deps = semantics.get_coverage_deps(ctx, runtime_details) else: @@ -1040,7 +1040,7 @@ def _declare_executable_file(ctx): return executable -def _get_runtime_details(ctx, semantics, is_test): +def _get_runtime_details(ctx, semantics): """Gets various information about the Python runtime to use. While most information comes from the toolchain, various legacy and @@ -1049,7 +1049,6 @@ def _get_runtime_details(ctx, semantics, is_test): Args: ctx: Rule ctx semantics: A `BinarySemantics` struct; see `create_binary_semantics_struct` - is_test: bool; True if the rule is a test rule (has `test=True`), False if not Returns: A struct; see inline-field comments of the return value for details. @@ -1078,7 +1077,6 @@ def _get_runtime_details(ctx, semantics, is_test): if not effective_runtime: fail("Unable to find Python runtime") - extra_test_env = {} if effective_runtime: direct = [] # List of files transitive = [] # List of depsets @@ -1091,12 +1089,6 @@ def _get_runtime_details(ctx, semantics, is_test): direct.append(effective_runtime.coverage_tool) if effective_runtime.coverage_files: transitive.append(effective_runtime.coverage_files) - if is_test: - py_test_toolchain = ctx.exec_groups["test"].toolchains[PY_TEST_TOOLCHAIN_TYPE] - if py_test_toolchain: - coverage_rc = py_test_toolchain.py_test_info.coverage_rc - extra_test_env = {"COVERAGE_RC": coverage_rc.files.to_list()[0].short_path} - direct.extend(coverage_rc.files.to_list()) runtime_files = depset(direct = direct, transitive = transitive) else: runtime_files = depset() @@ -1128,9 +1120,6 @@ def _get_runtime_details(ctx, semantics, is_test): # be included. For in-build runtimes, this shold include the interpreter # and any supporting files. runfiles = ctx.runfiles(transitive_files = runtime_files), - # extra_test_env: dict[str, str]; Additional environment variables to - # set when running the test. - extra_test_env = extra_test_env, ) def _maybe_get_runtime_from_ctx(ctx): @@ -1626,22 +1615,8 @@ def _create_providers( Returns: A list of modern providers. """ - providers = [ - DefaultInfo( - executable = executable, - files = default_outputs, - default_runfiles = _py_builtins.make_runfiles_respect_legacy_external_runfiles( - ctx, - runfiles_details.default_runfiles, - ), - data_runfiles = _py_builtins.make_runfiles_respect_legacy_external_runfiles( - ctx, - runfiles_details.data_runfiles, - ), - ), create_instrumented_files_info(ctx), - _create_run_environment_info(ctx, inherited_environment, runtime_details.extra_test_env), PyExecutableInfo( main = main_py, runfiles_without_exe = runfiles_details.runfiles_without_exe, @@ -1712,9 +1687,22 @@ def _create_providers( runtime_details = runtime_details, ) providers.extend(extra_providers) - return providers + environemnt_info = _create_run_environment_info(ctx, inherited_environment) + binary_info = struct( + executable = executable, + files = default_outputs, + default_runfiles = _py_builtins.make_runfiles_respect_legacy_external_runfiles( + ctx, + runfiles_details.default_runfiles, + ), + data_runfiles = _py_builtins.make_runfiles_respect_legacy_external_runfiles( + ctx, + runfiles_details.data_runfiles, + ), + ) + return providers, binary_info, environemnt_info -def _create_run_environment_info(ctx, inherited_environment, extra_test_env): +def _create_run_environment_info(ctx, inherited_environment): expanded_env = {} for key, value in ctx.attr.env.items(): expanded_env[key] = _py_builtins.expand_location_and_make_variables( @@ -1723,8 +1711,7 @@ def _create_run_environment_info(ctx, inherited_environment, extra_test_env): expression = value, targets = ctx.attr.data, ) - expanded_env.update(extra_test_env) - return RunEnvironmentInfo( + return struct( environment = expanded_env, inherited_environment = inherited_environment, ) diff --git a/python/private/py_test_rule.bzl b/python/private/py_test_rule.bzl index dcf55dd44d..5559f44845 100644 --- a/python/private/py_test_rule.bzl +++ b/python/private/py_test_rule.bzl @@ -41,13 +41,38 @@ _BAZEL_PY_TEST_ATTRS = { } def _py_test_impl(ctx): - providers = py_executable_impl( + providers, binary_info, environment_info = py_executable_impl( ctx = ctx, is_test = True, inherited_environment = ctx.attr.env_inherit, ) maybe_add_test_execution_info(providers, ctx) - return providers + py_test_toolchain = ctx.exec_groups["test"].toolchains[PY_TEST_TOOLCHAIN_TYPE] + if py_test_toolchain: + py_test_info = py_test_toolchain.py_test_info + else: + providers.extend( + [ + DefaultInfo( + executable = binary_info.executable, + files = binary_info.files, + default_runfiles = binary_info.default_runfiles, + data_runfiles = binary_info.data_runfiles, + ), + RunEnvironmentInfo( + environment = environment_info.environment, + inherited_environment = environment_info.inherited_environment, + ), + ], + ) + return providers + test_providers = py_test_info.get_runner.func( + ctx, + binary_info, + environment_info, + **py_test_info.get_runner.args + ) + return test_providers + providers py_test = create_executable_rule( implementation = _py_test_impl, diff --git a/python/private/py_test_toolchain.bzl b/python/private/py_test_toolchain.bzl index d8bc80ae6d..052f07c67d 100644 --- a/python/private/py_test_toolchain.bzl +++ b/python/private/py_test_toolchain.bzl @@ -25,15 +25,58 @@ load( PyTestProviderInfo = provider( doc = "Information about the pytest toolchain", fields = [ - "coverage_rc", + "get_runner", ], ) +def _get_runner(ctx, binary_info, environment_info, coverage_rc): + """ + Constructs and returns a list containing `DefaultInfo` and `RunEnvironmentInfo` for a test runner setup. + + Args: + ctx: The rule context, providing access to actions, inputs, outputs, and more. + binary_info: A `struct` with defaultinfo details. + - `executable`: The executable binary. + - `files`: The files associated with the binary. + - `default_runfiles`: The default runfiles of the binary. + - `data_runfiles`: Additional runfiles for data dependencies. + environment_info: A `struct` with environment details. + - `environment`: A dictionary of key-value pairs for the test environment. + - `inherited_environment`: A list of environment variables inherited from the host. + coverage_rc: A `File` or `File`-like target containing coverage configuration files. + """ + + test_env = {"COVERAGE_RC": coverage_rc.files.to_list()[0].short_path} + test_env.update(environment_info.environment) + + return [ + DefaultInfo( + # Opportunity to override the executable in the binary_info with a new testrunner. + executable = binary_info.executable, + files = binary_info.files, + default_runfiles = binary_info.default_runfiles.merge( + ctx.runfiles( + transitive_files = coverage_rc.files, + ), + ), + data_runfiles = binary_info.data_runfiles, + ), + RunEnvironmentInfo( + environment = test_env, + inherited_environment = environment_info.inherited_environment, + ), + ] + def _py_test_toolchain_impl(ctx): return [ platform_common.ToolchainInfo( py_test_info = PyTestProviderInfo( - coverage_rc = ctx.attr.coverage_rc, + get_runner = struct( + func = _get_runner, + args = { + "coverage_rc": ctx.attr.coverage_rc, + }, + ), ), ), ] @@ -41,9 +84,7 @@ def _py_test_toolchain_impl(ctx): py_test_toolchain = rule( implementation = _py_test_toolchain_impl, attrs = { - "coverage_rc": attr.label( - allow_single_file = True, - ), + "coverage_rc": attr.label(allow_single_file = True), }, ) @@ -94,9 +135,9 @@ py_test_toolchain_repo = repository_rule( doc = "Generates a toolchain hub repository", attrs = { "coverage_rc": attr.label( - allow_single_file = True, doc = "The coverage rc file", mandatory = True, + allow_single_file = True, ), "toolchain_type": attr.label(doc = "Toolchain type", mandatory = True), },