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 d684b9c31d..66776aea44 100644 --- a/examples/bzlmod/BUILD.bazel +++ b/examples/bzlmod/BUILD.bazel @@ -11,6 +11,8 @@ load("@python_3_9//:defs.bzl", py_test_with_transition = "py_test") load("@python_versions//3.10:defs.bzl", compile_pip_requirements_3_10 = "compile_pip_requirements") load("@rules_python//python:defs.bzl", "py_binary", "py_library", "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 e9d69c5ab8..7874575d8f 100644 --- a/examples/bzlmod/MODULE.bazel +++ b/examples/bzlmod/MODULE.bazel @@ -36,6 +36,10 @@ python.toolchain( configure_coverage_tool = True, python_version = "3.10", ) +python.converage( + name = "coverage", + coveragerc = ".coveragerc", +) # One can override the actual toolchain versions that are available, which can be useful # when optimizing what gets downloaded and when. @@ -89,7 +93,9 @@ python.single_version_platform_override( # See the tests folder for various examples on using multiple Python versions. # The names "python_3_9" and "python_3_10" are autmatically created by the repo # rules based on the `python_version` arg values. -use_repo(python, "python_3_10", "python_3_9", "python_versions", "pythons_hub") +use_repo(python, "coverage_py_test_toolchain", "python_3_10", "python_3_9", "python_versions", "pythons_hub") + +register_toolchains("@coverage_py_test_toolchain//:all") # EXPERIMENTAL: This is experimental and may be removed without notice uv = use_extension("@rules_python//python/uv:extensions.bzl", "uv") diff --git a/examples/bzlmod/tests/BUILD.bazel b/examples/bzlmod/tests/BUILD.bazel index 7cbc8d47b7..03d287a556 100644 --- a/examples/bzlmod/tests/BUILD.bazel +++ b/examples/bzlmod/tests/BUILD.bazel @@ -48,6 +48,12 @@ py_test( deps = ["//libs/my_lib"], ) +py_test( + name = "coverage_rc_is_set_test", + srcs = ["coverage_rc_is_set_test.py"], + main = "coverage_rc_is_set_test.py", +) + py_test_3_9( name = "my_lib_3_9_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 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 f2f3374db3..fbd6a8418f 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 ce1288cc29..d958078197 100644 --- a/python/private/py_executable.bzl +++ b/python/private/py_executable.bzl @@ -60,6 +60,7 @@ load( load( ":toolchain_types.bzl", "EXEC_TOOLS_TOOLCHAIN_TYPE", + "PY_TEST_TOOLCHAIN_TYPE", TOOLCHAIN_TYPE = "TARGET_TOOLCHAIN_TYPE", ) @@ -254,6 +255,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): @@ -819,7 +821,8 @@ def _create_providers( inherited_environment, runtime_details, output_groups, - semantics): + semantics, + is_test): """Creates the providers an executable should return. Args: @@ -851,13 +854,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, @@ -865,7 +879,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, @@ -937,7 +951,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( @@ -946,6 +960,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_bazel.bzl b/python/private/py_test_rule_bazel.bzl index 369360d90f..db75aa82cf 100644 --- a/python/private/py_test_rule_bazel.bzl +++ b/python/private/py_test_rule_bazel.bzl @@ -14,6 +14,7 @@ """Rule implementation of py_test for Bazel.""" 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..6ff44a89f7 --- /dev/null +++ b/python/private/py_test_toolchain.bzl @@ -0,0 +1,93 @@ +# 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") +py_test_toolchain( + name = "{name}_toolchain", + coverage_rc = "{coverage_rc}", +) + +toolchain( + name = "{name}", + target_compatible_with = [], + exec_compatible_with = [], + toolchain = "{name}_toolchain", + toolchain_type = "{toolchain_type}", +) +""" + +def _toolchains_repo_impl(repository_ctx): + build_content = _TOOLCHAIN_TEMPLATE.format( + name = "py_test_toolchain", + toolchain_type = repository_ctx.attr.toolchain_type, + coverage_rc = repository_ctx.attr.coverage_rc, + ) + 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 f66c28bd51..ab1b26c898 100644 --- a/python/private/stage2_bootstrap_template.py +++ b/python/private/stage2_bootstrap_template.py @@ -345,13 +345,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")