diff --git a/experimental/examples/pytest/BUILD b/experimental/examples/pytest/BUILD new file mode 100644 index 000000000..9ebf8ec13 --- /dev/null +++ b/experimental/examples/pytest/BUILD @@ -0,0 +1,46 @@ +# Copyright 2019 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. +package(default_visibility = ["//visibility:public"]) + +licenses(["notice"]) # Apache 2.0 + +load("@examples_pytest//:requirements.bzl", "requirement") +load("//experimental/python:pytest.bzl", "pytest_test") +load("//python:defs.bzl", "py_library") + +py_library( + name = "conftest", + srcs = ["conftest.py"], +) + +pytest_test( + name = "all_tests", + test_files = glob(["*_test.py"]), + deps = [ + ":conftest", + requirement("pytest"), + # These should be pulled in by requirement("pytest"), but it's currently broken + # See #110 and #90 + requirement("attrs"), + requirement("atomicwrites"), + requirement("importlib_metadata"), + requirement("more_itertools"), + requirement("packaging"), + requirement("pathlib2"), + requirement("pluggy"), + requirement("py"), + requirement("wcwidth"), + requirement("zipp"), + ], +) diff --git a/experimental/examples/pytest/conftest.py b/experimental/examples/pytest/conftest.py new file mode 100644 index 000000000..c64859782 --- /dev/null +++ b/experimental/examples/pytest/conftest.py @@ -0,0 +1,19 @@ +# Copyright 2019 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 pytest + +@pytest.fixture +def greeting(): + return "Hello Bazel!" diff --git a/experimental/examples/pytest/other_test.py b/experimental/examples/pytest/other_test.py new file mode 100644 index 000000000..f50b29818 --- /dev/null +++ b/experimental/examples/pytest/other_test.py @@ -0,0 +1,16 @@ +# Copyright 2019 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. + +def test_other(): + assert 4 == 2 + 2 diff --git a/experimental/examples/pytest/requirements.txt b/experimental/examples/pytest/requirements.txt new file mode 100644 index 000000000..74eb0e9b0 --- /dev/null +++ b/experimental/examples/pytest/requirements.txt @@ -0,0 +1,2 @@ +# Need https://github.com/pytest-dev/pytest/pull/4738 to work with Bazel. +pytest>=4.2.1 diff --git a/experimental/examples/pytest/sample_test.py b/experimental/examples/pytest/sample_test.py new file mode 100644 index 000000000..703acadee --- /dev/null +++ b/experimental/examples/pytest/sample_test.py @@ -0,0 +1,16 @@ +# Copyright 2019 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. + +def test_greeting(greeting): + assert "Bazel" in greeting diff --git a/experimental/python/BUILD b/experimental/python/BUILD index 8e8a05900..9ebc58971 100644 --- a/experimental/python/BUILD +++ b/experimental/python/BUILD @@ -15,4 +15,7 @@ package(default_visibility = ["//visibility:public"]) licenses(["notice"]) # Apache 2.0 -exports_files(["wheel.bzl"]) +exports_files([ + "wheel.bzl", + "pytest.bzl", +]) diff --git a/experimental/python/pytest.bzl b/experimental/python/pytest.bzl new file mode 100644 index 000000000..b6c21470e --- /dev/null +++ b/experimental/python/pytest.bzl @@ -0,0 +1,107 @@ +# Copyright 2018 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. + +"""Rules for converting py.test tests into bazel targets.""" + +load("//python:defs.bzl", "py_test") + +def _sanitize_name(filename): + return filename.replace("/", "__").replace(".", "_") + +def _pytest_runner_impl(ctx): + """Creates a wrapper script that runs py.test runner for given list of files.""" + runner_script = ctx.actions.declare_file(ctx.attr.name) + ctx.actions.write(runner_script, """\ +import sys +import pytest + +# sys.exit to propagate the exit code to bazel +# sys.argv[1:] to pass additional flags from bazel --test_arg to py.test, +# e.g. "bazel test --test_arg=-s :my_test" +sys.exit(pytest.main(sys.argv[1:] + %s)) +""" % repr(ctx.attr.test_files)) + return [DefaultInfo(executable = runner_script)] + +pytest_runner = rule( + implementation = _pytest_runner_impl, + attrs = { + "test_files": attr.string_list(mandatory = True, allow_empty = False), + }, + doc = """Creates a wrapper script that runs py.test runner for given list of files. + + This is an implementation detail for pytest_test() macro. Use pytest_test() instead. + """, + executable = True, +) + +def _make_pytest_target(name, test_files, **kwargs): + """Instantiate pytest_runner rule and a corresponding py_test rule. + + Args: + name: Name of the rule + test_files: List of py.test files to be executed. + **kwargs: Additional arguments to pass to the py_test targets, e.g. deps. + """ + abs_test_files = [ + native.package_name() + "/" + test_file + for test_file in test_files + ] + runner_file = name + "_runner.py" + pytest_runner( + name = runner_file, + test_files = abs_test_files, + ) + py_test( + name = name, + srcs = [runner_file] + test_files, + main = runner_file, + **kwargs + ) + +def pytest_test(name, test_files, **kwargs): + """Create bazel native py_test rules for tests using py.test framework. + + Args: + name: name of the generated test rule, + test_files: Python test files to run, + **kwargs: other arguments (e.g. "deps") are passed to py_test rule. + + Make sure that "deps" include: + - py_library with pytest, e.g. created by pip_import. + - py_library target that contains conftest.py file, if you use conftest. + + Example: + py_library( + name = "conftest", + srcs = ["conftest.py"], + ) + + pytest_test( + name = "all_test", + test_files = glob(["*_test.py"]), + deps = [ + ":conftest", + requirement("pytest"), + ], + ) + """ + if len(test_files) > 1: + for test_file in test_files: + _make_pytest_target( + name = name + "_" + _sanitize_name(test_file), + test_files = [test_file], + **kwargs + ) + else: + _make_pytest_target(name, test_files, **kwargs) diff --git a/internal_deps.bzl b/internal_deps.bzl index 8e3df656d..97ee7bfc1 100644 --- a/internal_deps.bzl +++ b/internal_deps.bzl @@ -48,3 +48,7 @@ def examples(): name = "examples_extras", requirements = "@rules_python//examples/extras:requirements.txt", ) + pip_import( + name = "examples_pytest", + requirements = "@rules_python//experimental/examples/pytest:requirements.txt", + ) diff --git a/internal_setup.bzl b/internal_setup.bzl index c8df847d0..d00155dc9 100644 --- a/internal_setup.bzl +++ b/internal_setup.bzl @@ -14,6 +14,10 @@ load( "@examples_helloworld//:requirements.bzl", _helloworld_install = "pip_install", ) +load( + "@examples_pytest//:requirements.bzl", + _pytest_install = "pip_install", +) load( "@examples_version//:requirements.bzl", _version_install = "pip_install", @@ -36,3 +40,4 @@ def rules_python_internal_setup(): _version_install() _boto_install() _extras_install() + _pytest_install()