tests(pystar): py_runtime_pair and py_runtime analysis tests (#1441)
These analysis tests verify that `py_runtime` and `py_runtime_pair` are
working as intended
for both the native Bazel and Starlark implementations.
Work towards #1069
diff --git a/python/BUILD.bazel b/python/BUILD.bazel
index f9c93e5..3884349 100644
--- a/python/BUILD.bazel
+++ b/python/BUILD.bazel
@@ -147,7 +147,11 @@
bzl_library(
name = "py_runtime_pair_bzl",
srcs = ["py_runtime_pair.bzl"],
- deps = ["//python/private:bazel_tools_bzl"],
+ deps = [
+ "//python/private:bazel_tools_bzl",
+ "//python/private:py_runtime_pair_macro_bzl",
+ "@rules_python_internal//:rules_python_config_bzl",
+ ],
)
bzl_library(
diff --git a/python/private/BUILD.bazel b/python/private/BUILD.bazel
index a67183e..f0eddad 100644
--- a/python/private/BUILD.bazel
+++ b/python/private/BUILD.bazel
@@ -104,7 +104,8 @@
bzl_library(
name = "py_runtime_pair_macro_bzl",
- srcs = ["py_runtime_pair_rule.bzl"],
+ srcs = ["py_runtime_pair_macro.bzl"],
+ visibility = ["//:__subpackages__"],
deps = [":py_runtime_pair_rule_bzl"],
)
diff --git a/python/py_runtime_pair.bzl b/python/py_runtime_pair.bzl
index 951c606..c80994c 100644
--- a/python/py_runtime_pair.bzl
+++ b/python/py_runtime_pair.bzl
@@ -14,7 +14,11 @@
"""Public entry point for py_runtime_pair."""
-load("@bazel_tools//tools/python:toolchain.bzl", _py_runtime_pair = "py_runtime_pair")
+load("@bazel_tools//tools/python:toolchain.bzl", _bazel_tools_impl = "py_runtime_pair")
+load("@rules_python_internal//:rules_python_config.bzl", "config")
+load("//python/private:py_runtime_pair_macro.bzl", _starlark_impl = "py_runtime_pair")
+
+_py_runtime_pair = _bazel_tools_impl if not config.enable_pystar else _starlark_impl
# NOTE: This doc is copy/pasted from the builtin py_runtime_pair rule so our
# doc generator gives useful API docs.
diff --git a/tests/py_runtime/BUILD.bazel b/tests/py_runtime/BUILD.bazel
new file mode 100644
index 0000000..e097f0d
--- /dev/null
+++ b/tests/py_runtime/BUILD.bazel
@@ -0,0 +1,17 @@
+# 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.
+
+load(":py_runtime_tests.bzl", "py_runtime_test_suite")
+
+py_runtime_test_suite(name = "py_runtime_tests")
diff --git a/tests/py_runtime/py_runtime_tests.bzl b/tests/py_runtime/py_runtime_tests.bzl
new file mode 100644
index 0000000..662909c
--- /dev/null
+++ b/tests/py_runtime/py_runtime_tests.bzl
@@ -0,0 +1,262 @@
+# 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.
+"""Starlark tests for py_runtime rule."""
+
+load("@rules_python_internal//:rules_python_config.bzl", "config")
+load("@rules_testing//lib:analysis_test.bzl", "analysis_test")
+load("@rules_testing//lib:test_suite.bzl", "test_suite")
+load("@rules_testing//lib:truth.bzl", "matching")
+load("@rules_testing//lib:util.bzl", rt_util = "util")
+load("//python:py_runtime.bzl", "py_runtime")
+load("//python:py_runtime_info.bzl", "PyRuntimeInfo")
+load("//tests:py_runtime_info_subject.bzl", "py_runtime_info_subject")
+load("//tests/base_rules:util.bzl", br_util = "util")
+
+_tests = []
+
+_SKIP_TEST = {
+ "target_compatible_with": ["@platforms//:incompatible"],
+}
+
+def _test_bootstrap_template(name):
+ # The bootstrap_template arg isn't present in older Bazel versions, so
+ # we have to conditionally pass the arg and mark the test incompatible.
+ if config.enable_pystar:
+ py_runtime_kwargs = {"bootstrap_template": "bootstrap.txt"}
+ attr_values = {}
+ else:
+ py_runtime_kwargs = {}
+ attr_values = _SKIP_TEST
+
+ rt_util.helper_target(
+ py_runtime,
+ name = name + "_subject",
+ interpreter_path = "/py",
+ python_version = "PY3",
+ **py_runtime_kwargs
+ )
+ analysis_test(
+ name = name,
+ target = name + "_subject",
+ impl = _test_bootstrap_template_impl,
+ attr_values = attr_values,
+ )
+
+def _test_bootstrap_template_impl(env, target):
+ env.expect.that_target(target).provider(
+ PyRuntimeInfo,
+ factory = py_runtime_info_subject,
+ ).bootstrap_template().path().contains("bootstrap.txt")
+
+_tests.append(_test_bootstrap_template)
+
+def _test_cannot_have_both_inbuild_and_system_interpreter(name):
+ if br_util.is_bazel_6_or_higher():
+ py_runtime_kwargs = {
+ "interpreter": "fake_interpreter",
+ "interpreter_path": "/some/path",
+ }
+ attr_values = {}
+ else:
+ py_runtime_kwargs = {
+ "interpreter_path": "/some/path",
+ }
+ attr_values = _SKIP_TEST
+ rt_util.helper_target(
+ py_runtime,
+ name = name + "_subject",
+ python_version = "PY3",
+ **py_runtime_kwargs
+ )
+ analysis_test(
+ name = name,
+ target = name + "_subject",
+ impl = _test_cannot_have_both_inbuild_and_system_interpreter_impl,
+ expect_failure = True,
+ attr_values = attr_values,
+ )
+
+def _test_cannot_have_both_inbuild_and_system_interpreter_impl(env, target):
+ env.expect.that_target(target).failures().contains_predicate(
+ matching.str_matches("one of*interpreter*interpreter_path"),
+ )
+
+_tests.append(_test_cannot_have_both_inbuild_and_system_interpreter)
+
+def _test_cannot_specify_files_for_system_interpreter(name):
+ if br_util.is_bazel_6_or_higher():
+ py_runtime_kwargs = {"files": ["foo.txt"]}
+ attr_values = {}
+ else:
+ py_runtime_kwargs = {}
+ attr_values = _SKIP_TEST
+ rt_util.helper_target(
+ py_runtime,
+ name = name + "_subject",
+ interpreter_path = "/foo",
+ python_version = "PY3",
+ **py_runtime_kwargs
+ )
+ analysis_test(
+ name = name,
+ target = name + "_subject",
+ impl = _test_cannot_specify_files_for_system_interpreter_impl,
+ expect_failure = True,
+ attr_values = attr_values,
+ )
+
+def _test_cannot_specify_files_for_system_interpreter_impl(env, target):
+ env.expect.that_target(target).failures().contains_predicate(
+ matching.str_matches("files*must be empty"),
+ )
+
+_tests.append(_test_cannot_specify_files_for_system_interpreter)
+
+def _test_in_build_interpreter(name):
+ rt_util.helper_target(
+ py_runtime,
+ name = name + "_subject",
+ interpreter = "fake_interpreter",
+ python_version = "PY3",
+ files = ["file1.txt"],
+ )
+ analysis_test(
+ name = name,
+ target = name + "_subject",
+ impl = _test_in_build_interpreter_impl,
+ )
+
+def _test_in_build_interpreter_impl(env, target):
+ info = env.expect.that_target(target).provider(PyRuntimeInfo, factory = py_runtime_info_subject)
+ info.python_version().equals("PY3")
+ info.files().contains_predicate(matching.file_basename_equals("file1.txt"))
+ info.interpreter().path().contains("fake_interpreter")
+
+_tests.append(_test_in_build_interpreter)
+
+def _test_must_have_either_inbuild_or_system_interpreter(name):
+ if br_util.is_bazel_6_or_higher():
+ py_runtime_kwargs = {}
+ attr_values = {}
+ else:
+ py_runtime_kwargs = {
+ "interpreter_path": "/some/path",
+ }
+ attr_values = _SKIP_TEST
+ rt_util.helper_target(
+ py_runtime,
+ name = name + "_subject",
+ python_version = "PY3",
+ **py_runtime_kwargs
+ )
+ analysis_test(
+ name = name,
+ target = name + "_subject",
+ impl = _test_must_have_either_inbuild_or_system_interpreter_impl,
+ expect_failure = True,
+ attr_values = attr_values,
+ )
+
+def _test_must_have_either_inbuild_or_system_interpreter_impl(env, target):
+ env.expect.that_target(target).failures().contains_predicate(
+ matching.str_matches("one of*interpreter*interpreter_path"),
+ )
+
+_tests.append(_test_must_have_either_inbuild_or_system_interpreter)
+
+def _test_python_version_required(name):
+ # Bazel 5.4 will entirely crash when python_version is missing.
+ if br_util.is_bazel_6_or_higher():
+ py_runtime_kwargs = {}
+ attr_values = {}
+ else:
+ py_runtime_kwargs = {"python_version": "PY3"}
+ attr_values = _SKIP_TEST
+ rt_util.helper_target(
+ py_runtime,
+ name = name + "_subject",
+ interpreter_path = "/math/pi",
+ **py_runtime_kwargs
+ )
+ analysis_test(
+ name = name,
+ target = name + "_subject",
+ impl = _test_python_version_required_impl,
+ expect_failure = True,
+ attr_values = attr_values,
+ )
+
+def _test_python_version_required_impl(env, target):
+ env.expect.that_target(target).failures().contains_predicate(
+ matching.str_matches("must be set*PY2*PY3"),
+ )
+
+_tests.append(_test_python_version_required)
+
+def _test_system_interpreter(name):
+ rt_util.helper_target(
+ py_runtime,
+ name = name + "_subject",
+ interpreter_path = "/system/python",
+ python_version = "PY3",
+ )
+ analysis_test(
+ name = name,
+ target = name + "_subject",
+ impl = _test_system_interpreter_impl,
+ )
+
+def _test_system_interpreter_impl(env, target):
+ env.expect.that_target(target).provider(
+ PyRuntimeInfo,
+ factory = py_runtime_info_subject,
+ ).interpreter_path().equals("/system/python")
+
+_tests.append(_test_system_interpreter)
+
+def _test_system_interpreter_must_be_absolute(name):
+ # Bazel 5.4 will entirely crash when an invalid interpreter_path
+ # is given.
+ if br_util.is_bazel_6_or_higher():
+ py_runtime_kwargs = {"interpreter_path": "relative/path"}
+ attr_values = {}
+ else:
+ py_runtime_kwargs = {"interpreter_path": "/junk/value/for/bazel5.4"}
+ attr_values = _SKIP_TEST
+ rt_util.helper_target(
+ py_runtime,
+ name = name + "_subject",
+ python_version = "PY3",
+ **py_runtime_kwargs
+ )
+ analysis_test(
+ name = name,
+ target = name + "_subject",
+ impl = _test_system_interpreter_must_be_absolute_impl,
+ expect_failure = True,
+ attr_values = attr_values,
+ )
+
+def _test_system_interpreter_must_be_absolute_impl(env, target):
+ env.expect.that_target(target).failures().contains_predicate(
+ matching.str_matches("must be*absolute"),
+ )
+
+_tests.append(_test_system_interpreter_must_be_absolute)
+
+def py_runtime_test_suite(name):
+ test_suite(
+ name = name,
+ tests = _tests,
+ )
diff --git a/tests/py_runtime_info_subject.bzl b/tests/py_runtime_info_subject.bzl
new file mode 100644
index 0000000..9f42d3a
--- /dev/null
+++ b/tests/py_runtime_info_subject.bzl
@@ -0,0 +1,101 @@
+# 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.
+"""PyRuntimeInfo testing subject."""
+
+load("@rules_testing//lib:truth.bzl", "subjects")
+
+def py_runtime_info_subject(info, *, meta):
+ """Creates a new `PyRuntimeInfoSubject` for a PyRuntimeInfo provider instance.
+
+ Method: PyRuntimeInfoSubject.new
+
+ Args:
+ info: The PyRuntimeInfo object
+ meta: ExpectMeta object.
+
+ Returns:
+ A `PyRuntimeInfoSubject` struct
+ """
+
+ # buildifier: disable=uninitialized
+ public = struct(
+ # go/keep-sorted start
+ bootstrap_template = lambda *a, **k: _py_runtime_info_subject_bootstrap_template(self, *a, **k),
+ coverage_files = lambda *a, **k: _py_runtime_info_subject_coverage_files(self, *a, **k),
+ coverage_tool = lambda *a, **k: _py_runtime_info_subject_coverage_tool(self, *a, **k),
+ files = lambda *a, **k: _py_runtime_info_subject_files(self, *a, **k),
+ interpreter = lambda *a, **k: _py_runtime_info_subject_interpreter(self, *a, **k),
+ interpreter_path = lambda *a, **k: _py_runtime_info_subject_interpreter_path(self, *a, **k),
+ python_version = lambda *a, **k: _py_runtime_info_subject_python_version(self, *a, **k),
+ stub_shebang = lambda *a, **k: _py_runtime_info_subject_stub_shebang(self, *a, **k),
+ # go/keep-sorted end
+ )
+ self = struct(
+ actual = info,
+ meta = meta,
+ )
+ return public
+
+def _py_runtime_info_subject_bootstrap_template(self):
+ return subjects.file(
+ self.actual.bootstrap_template,
+ meta = self.meta.derive("bootstrap_template()"),
+ )
+
+def _py_runtime_info_subject_coverage_files(self):
+ """Returns a `DepsetFileSubject` for the `coverage_files` attribute.
+
+ Args:
+ self: implicitly added.
+ """
+ return subjects.depset_file(
+ self.actual.coverage_files,
+ meta = self.meta.derive("coverage_files()"),
+ )
+
+def _py_runtime_info_subject_coverage_tool(self):
+ return subjects.file(
+ self.actual.coverage_tool,
+ meta = self.meta.derive("coverage_tool()"),
+ )
+
+def _py_runtime_info_subject_files(self):
+ return subjects.depset_file(
+ self.actual.files,
+ meta = self.meta.derive("files()"),
+ )
+
+def _py_runtime_info_subject_interpreter(self):
+ return subjects.file(
+ self.actual.interpreter,
+ meta = self.meta.derive("interpreter()"),
+ )
+
+def _py_runtime_info_subject_interpreter_path(self):
+ return subjects.str(
+ self.actual.interpreter_path,
+ meta = self.meta.derive("interpreter_path()"),
+ )
+
+def _py_runtime_info_subject_python_version(self):
+ return subjects.str(
+ self.actual.python_version,
+ meta = self.meta.derive("python_version()"),
+ )
+
+def _py_runtime_info_subject_stub_shebang(self):
+ return subjects.str(
+ self.actual.stub_shebang,
+ meta = self.meta.derive("stub_shebang()"),
+ )
diff --git a/tests/py_runtime_pair/BUILD.bazel b/tests/py_runtime_pair/BUILD.bazel
new file mode 100644
index 0000000..6a6a4b9
--- /dev/null
+++ b/tests/py_runtime_pair/BUILD.bazel
@@ -0,0 +1,17 @@
+# 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.
+
+load(":py_runtime_pair_tests.bzl", "py_runtime_pair_test_suite")
+
+py_runtime_pair_test_suite(name = "py_runtime_pair_tests")
diff --git a/tests/py_runtime_pair/py_runtime_pair_tests.bzl b/tests/py_runtime_pair/py_runtime_pair_tests.bzl
new file mode 100644
index 0000000..e1ff19e
--- /dev/null
+++ b/tests/py_runtime_pair/py_runtime_pair_tests.bzl
@@ -0,0 +1,66 @@
+# 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.
+"""Starlark tests for py_runtime_pair rule."""
+
+load("@rules_testing//lib:analysis_test.bzl", "analysis_test")
+load("@rules_testing//lib:test_suite.bzl", "test_suite")
+load("@rules_testing//lib:truth.bzl", "matching", "subjects")
+load("@rules_testing//lib:util.bzl", rt_util = "util")
+load("//python:py_runtime.bzl", "py_runtime")
+load("//python:py_runtime_pair.bzl", "py_runtime_pair")
+load("//tests:py_runtime_info_subject.bzl", "py_runtime_info_subject")
+
+_tests = []
+
+def _test_basic(name):
+ rt_util.helper_target(
+ py_runtime,
+ name = name + "_runtime",
+ interpreter = "fake_interpreter",
+ python_version = "PY3",
+ files = ["file1.txt"],
+ )
+ rt_util.helper_target(
+ py_runtime_pair,
+ name = name + "_subject",
+ py3_runtime = name + "_runtime",
+ )
+ analysis_test(
+ name = name,
+ target = name + "_subject",
+ impl = _test_basic_impl,
+ )
+
+def _test_basic_impl(env, target):
+ toolchain = env.expect.that_target(target).provider(
+ platform_common.ToolchainInfo,
+ factory = lambda value, meta: subjects.struct(
+ value,
+ meta = meta,
+ attrs = {
+ "py3_runtime": py_runtime_info_subject,
+ },
+ ),
+ )
+ toolchain.py3_runtime().python_version().equals("PY3")
+ toolchain.py3_runtime().files().contains_predicate(matching.file_basename_equals("file1.txt"))
+ toolchain.py3_runtime().interpreter().path().contains("fake_interpreter")
+
+_tests.append(_test_basic)
+
+def py_runtime_pair_test_suite(name):
+ test_suite(
+ name = name,
+ tests = _tests,
+ )