test(pytest): support python_versions in pytest_test macro (#4064)
test(pytest): support python_versions in pytest_test macro
Testing pytest-based suites across multiple Python versions previously
required manually defining individual targets for each Python version.
Update the \`pytest_test\` macro in test support helpers to accept an
optional \`python_versions\` list. When specified, version-specific
\`pytest_test\` targets with formatted names are generated and grouped
under a root \`test_suite\`.
diff --git a/tests/pytest_test/BUILD.bazel b/tests/pytest_test/BUILD.bazel
index b15094a..204671c 100644
--- a/tests/pytest_test/BUILD.bazel
+++ b/tests/pytest_test/BUILD.bazel
@@ -1,5 +1,11 @@
load("//tests/support:support.bzl", "SUPPORTS_BZLMOD")
-load("//tests/support/pytest_test:pytest_test.bzl", "pytest_test")
+load(
+ "//tests/support/pytest_test:pytest_test.bzl",
+ "pytest_test",
+)
+load(":pytest_test_tests.bzl", "pytest_test_test_suite")
+
+pytest_test_test_suite(name = "pytest_test_tests")
pytest_test(
name = "pytest_script_venv_test",
@@ -20,3 +26,15 @@
],
target_compatible_with = SUPPORTS_BZLMOD,
)
+
+pytest_test(
+ name = "pytest_multipy_default_test",
+ srcs = [
+ "basic_test.py",
+ ],
+ python_versions = [
+ "3.14",
+ "3.13",
+ ],
+ target_compatible_with = SUPPORTS_BZLMOD,
+)
diff --git a/tests/pytest_test/pytest_test_tests.bzl b/tests/pytest_test/pytest_test_tests.bzl
new file mode 100644
index 0000000..8699ac2
--- /dev/null
+++ b/tests/pytest_test/pytest_test_tests.bzl
@@ -0,0 +1,37 @@
+"""Tests for pytest_test."""
+
+load("@rules_testing//lib:test_suite.bzl", "test_suite")
+load(
+ "//tests/support/pytest_test:pytest_test.bzl",
+ "get_version_test_name",
+)
+
+_tests = []
+
+def _test_get_version_test_name(env):
+ want = {
+ ("foo_test", "3.14"): "foo_py3.14_test",
+ ("foo_test", "3.10"): "foo_py3.10_test",
+ ("foo_test", "py3.14"): "foo_py3.14_test",
+ ("foo_tests", "3.14"): "foo_py3.14_tests",
+ ("foo", "3.14"): "foo_py3.14",
+ ("test_foo", "3.14"): "test_foo_py3.14",
+ ("basic_test", "3.11"): "basic_py3.11_test",
+ ("pytest_default_test", "3.12"): "pytest_default_py3.12_test",
+ }
+
+ actual = {
+ (name, ver): get_version_test_name(name, ver)
+ for (name, ver) in want.keys()
+ }
+ env.expect.that_dict(actual).contains_exactly(want)
+
+_tests.append(_test_get_version_test_name)
+
+def pytest_test_test_suite(name):
+ """Create the test suite.
+
+ Args:
+ name: The name of the test suite.
+ """
+ test_suite(name = name, basic_tests = _tests)
diff --git a/tests/support/pytest_test/pytest_test.bzl b/tests/support/pytest_test/pytest_test.bzl
index 893b22e..bbdfffb 100644
--- a/tests/support/pytest_test/pytest_test.bzl
+++ b/tests/support/pytest_test/pytest_test.bzl
@@ -11,6 +11,7 @@
srcs,
pytest = None,
pytest_bazel = None,
+ python_versions = None,
**kwargs):
"""Run pytest tests.
@@ -21,9 +22,76 @@
pytest: The pytest target to use. Defaults to @pypi//pytest.
pytest_bazel: The pytest-bazel target to use. Defaults to
@pypi//pytest_bazel.
+ python_versions: List of Python versions to test against. If specified,
+ a test is created for each version and grouped under a test_suite
+ named `name`.
**kwargs: Additional arguments passed to py_test. Note that `main` is
not a supported argument.
"""
+ if python_versions != None:
+ _multi_pytest_test(
+ name = name,
+ srcs = srcs,
+ pytest = pytest,
+ pytest_bazel = pytest_bazel,
+ python_versions = python_versions,
+ **kwargs
+ )
+ else:
+ _single_pytest_test(
+ name = name,
+ srcs = srcs,
+ pytest = pytest,
+ pytest_bazel = pytest_bazel,
+ **kwargs
+ )
+
+def _multi_pytest_test(
+ *,
+ name,
+ srcs,
+ pytest = None,
+ pytest_bazel = None,
+ python_versions,
+ **kwargs):
+ if "python_version" in kwargs:
+ fail(
+ "Cannot specify both python_version and python_versions in " +
+ "pytest_test; use one or the other.",
+ )
+ if not python_versions:
+ fail("python_versions must not be empty for {}".format(name))
+
+ tests = []
+ for python_version in python_versions:
+ test_name = _get_version_test_name(name, python_version)
+ _single_pytest_test(
+ name = test_name,
+ srcs = srcs,
+ pytest = pytest,
+ pytest_bazel = pytest_bazel,
+ python_version = python_version,
+ **kwargs
+ )
+ tests.append(":" + test_name)
+
+ test_suite_kwargs = {}
+ if "visibility" in kwargs:
+ test_suite_kwargs["visibility"] = kwargs["visibility"]
+
+ native.test_suite(
+ name = name,
+ tests = tests,
+ **test_suite_kwargs
+ )
+
+def _single_pytest_test(
+ *,
+ name,
+ srcs,
+ pytest = None,
+ pytest_bazel = None,
+ **kwargs):
if pytest == None:
pytest = _DEFAULT_PYTEST
if pytest_bazel == None:
@@ -37,17 +105,32 @@
output_name = main_file,
)
+ kwargs = dict(kwargs)
+ deps = kwargs.pop("deps", [])
py_test(
name = name,
main = main_file,
srcs = [bootstrap_target] + srcs,
- deps = kwargs.pop("deps", []) + [
+ deps = deps + [
pytest,
pytest_bazel,
],
**kwargs
)
+def _get_version_test_name(name, python_version):
+ version_str = str(python_version)
+ if not version_str.startswith("py"):
+ version_str = "py" + version_str
+
+ if name.endswith("_test"):
+ return "{}_{}_test".format(name[:-len("_test")], version_str)
+ elif name.endswith("_tests"):
+ return "{}_{}_tests".format(name[:-len("_tests")], version_str)
+ return "{}_{}".format(name, version_str)
+
+get_version_test_name = _get_version_test_name
+
def _write_pytest_bootstrap_impl(ctx):
output = ctx.actions.declare_file(ctx.attr.output_name)
test_files = "\n".join([f.short_path for f in ctx.files.srcs])