From 2dcb40799f11fa3ec50cd0957da418060fbb361a Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 12 Dec 2020 11:45:40 -0300 Subject: [PATCH 001/113] Setup GitHub Actions --- .github/workflows/main.yml | 99 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 .github/workflows/main.yml diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 00000000..df2735ba --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,99 @@ +name: build + +on: [push, pull_request] + +jobs: + build: + + runs-on: ${{ matrix.os }} + + strategy: + fail-fast: false + matrix: + tox_env: + - "py35-pytestlatest" + - "py36-pytestlatest" + - "py37-pytestlatest" + - "py38-pytestlatest" + - "py39-pytestlatest" + - "py38-pytestmaster" + - "py38-psutil" + - "linting" + + os: [ubuntu-latest, windows-latest] + include: + - tox_env: "py35-pytestlatest" + python: "3.5" + - tox_env: "py36-pytestlatest" + python: "3.6" + - tox_env: "py37-pytestlatest" + python: "3.7" + - tox_env: "py38-pytestlatest" + python: "3.8" + - tox_env: "py39-pytestlatest" + python: "3.9" + - tox_env: "py38-pytestmaster" + python: "3.8" + - tox_env: "py38-psutil" + python: "3.8" + - tox_env: "linting" + python: "3.8" + + steps: + - uses: actions/checkout@v1 + - name: Set up Python + uses: actions/setup-python@v1 + with: + python-version: ${{ matrix.python }} + - name: Install tox + run: | + python -m pip install --upgrade pip + pip install tox + - name: Test + run: | + tox -e ${{ matrix.tox_env }} + + linting: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v1 + - name: Set up Python + uses: actions/setup-python@v1 + with: + python-version: "3.7" + - name: Install tox + run: | + python -m pip install --upgrade pip + pip install tox + - name: Linting + run: | + tox -e linting + + deploy: + + if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') + + runs-on: ubuntu-latest + + needs: [build, linting] + + steps: + - uses: actions/checkout@v1 + - name: Set up Python + uses: actions/setup-python@v1 + with: + python-version: "3.7" + - name: Install wheel + run: | + python -m pip install --upgrade pip + pip install wheel + - name: Build package + run: | + python setup.py sdist bdist_wheel + - name: Publish package to PyPI + uses: pypa/gh-action-pypi-publish@master + with: + user: __token__ + password: ${{ secrets.pypi_token }} From 16694a978d81e1d5a7cd0b07d92937591310400d Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 12 Dec 2020 11:52:37 -0300 Subject: [PATCH 002/113] Pass PRE_COMMIT_HOME to reuse local cache --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index 3774b08a..af7aa4f3 100644 --- a/tox.ini +++ b/tox.ini @@ -24,6 +24,7 @@ commands = [testenv:linting] skip_install = True usedevelop = True +passenv = PRE_COMMIT_HOME deps = pre-commit commands = pre-commit run --all-files --show-diff-on-failure From 3fb369c8ac1b7514cc770ae193daec7ead24569e Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 12 Dec 2020 11:52:47 -0300 Subject: [PATCH 003/113] Fix linting --- .github/workflows/main.yml | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index df2735ba..4e273197 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -10,16 +10,16 @@ jobs: strategy: fail-fast: false matrix: - tox_env: - - "py35-pytestlatest" - - "py36-pytestlatest" - - "py37-pytestlatest" - - "py38-pytestlatest" - - "py39-pytestlatest" - - "py38-pytestmaster" - - "py38-psutil" + tox_env: + - "py35-pytestlatest" + - "py36-pytestlatest" + - "py37-pytestlatest" + - "py38-pytestlatest" + - "py39-pytestlatest" + - "py38-pytestmaster" + - "py38-psutil" - "linting" - + os: [ubuntu-latest, windows-latest] include: - tox_env: "py35-pytestlatest" From b3a1bf3c2e8c46c7f07d3be34f66f704397c2076 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 12 Dec 2020 11:58:41 -0300 Subject: [PATCH 004/113] Fix test_warning_captured_deprecated_in_pytest_6 The test was failing because *other* warnings were being triggered on the master node. This makes the test more reliable by checking only that the warning from the worker node is not emitted. Fix #601 --- testing/acceptance_test.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index 48ed35f9..b71d1d94 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -772,19 +772,23 @@ def test_warning_captured_deprecated_in_pytest_6(self, testdir): testdir.makeconftest( """ - def pytest_warning_captured(): - assert False, "this hook should not be called in this version" + def pytest_warning_captured(warning_message): + if warning_message == "my custom worker warning": + assert False, ( + "this hook should not be called from workers " + "in this version: {}" + ).format(warning_message) """ ) testdir.makepyfile( """ import warnings def test(): - warnings.warn("custom warning") + warnings.warn("my custom worker warning") """ ) result = testdir.runpytest("-n1") - result.stdout.fnmatch_lines(["* 1 passed in *"]) + result.stdout.fnmatch_lines(["*1 passed*"]) result.stdout.no_fnmatch_line("*this hook should not be called in this version") @pytest.mark.parametrize("n", ["-n0", "-n1"]) From 00b0464d4b00339345f03b774f7d48ed9f99dc8e Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 12 Dec 2020 12:01:16 -0300 Subject: [PATCH 005/113] Remove travis and appveyor configs --- .appveyor.yml | 21 ----------------- .travis.yml | 64 --------------------------------------------------- 2 files changed, 85 deletions(-) delete mode 100644 .appveyor.yml delete mode 100644 .travis.yml diff --git a/.appveyor.yml b/.appveyor.yml deleted file mode 100644 index 667bf4ae..00000000 --- a/.appveyor.yml +++ /dev/null @@ -1,21 +0,0 @@ -environment: - matrix: - - TOXENV: "py35-pytestlatest" - - TOXENV: "py36-pytestlatest" - - TOXENV: "py37-pytestlatest" - - TOXENV: "py38-pytestlatest" - - TOXENV: "py38-pytestmaster" - - TOXENV: "py38-psutil" - -install: - - C:\Python38\python -m pip install -U pip setuptools virtualenv - - C:\Python38\python -m pip install -U tox setuptools_scm - -build: false # Not a C# project, build stuff at the test step instead. - -test_script: - - C:\Python38\python -m tox - -# We don't deploy anything on tags with AppVeyor, we use Travis instead, so we -# might as well save resources -skip_tags: true diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 9f728eb2..00000000 --- a/.travis.yml +++ /dev/null @@ -1,64 +0,0 @@ -dist: xenial -language: python - -notifications: - irc: - channels: - - 'chat.freenode.net#pytest' - on_success: change - on_failure: change - skip_join: true - email: - - pytest-commit@python.org - -install: - - pip install -U pip setuptools - - pip install tox setuptools_scm -script: tox - -stages: - - baseline - - test - - name: deploy - if: repo = pytest-dev/pytest-xdist AND tag IS present - -jobs: - include: - - stage: baseline - python: '3.7' - env: TOXENV=linting - cache: - directories: - - $HOME/.cache/pre-commit - - python: '3.8' - env: TOXENV=py38-pytestlatest - - - stage: test - python: "3.5" - env: TOXENV=py35-pytestlatest - - python: "3.6" - env: TOXENV=py36-pytestlatest - - python: "3.7" - env: TOXENV=py37-pytestlatest - - python: "3.9-dev" - env: TOXENV=py39-pytestlatest - - python: "3.8" - env: TOXENV=py38-pytestmaster - - python: "3.8" - env: TOXENV=py38-psutil - - - stage: deploy - python: '3.8' - env: - install: pip install -U setuptools setuptools_scm - script: skip - deploy: - provider: pypi - user: ronny - distributions: sdist bdist_wheel - skip_upload_docs: true - password: - secure: cxmSDho5d+PYKEM4ZCg8ms1P4lzhYkrw6fEOm2HtTcsuCyY6aZMSgImWAnEYbJHSkdzgcxlXK9UKJ9B0YenXmBCkAr7UjdnpNXNmkySr0sYzlH/sfqt/dDATCHFaRKxnkOSOVywaDYhT9n8YudbXI77pXwD12i/CeSSJDbHhsu0JYUfAcb+D6YjRYoA2SEGCnzSzg+gDDfwXZx4ZiODCGLVwieNp1klCg88YROUE1BaYYNuUOONvfXX8+TWowbCF6ChH1WL/bZ49OStEYQNuYxZQZr4yClIqu9VJbchrU8j860K9ott2kkGTgfB/dDrQB/XncBubyIX9ikzCQAmmBXWAI3eyvWLPDk2Jz7kW2l2RT7syct80tCq3JhvQ1qdwr5ap7siocTLgnBW0tF4tkHSTFN3510fkc43npnp6FThebESQpnI24vqpwJ9hI/kW5mYi014Og2E/cpCXnz2XO8iZPDbqAMQpDsqEQoyhfGNgPTGp4K30TxRtwZBI5hHhDKnnR16fXtRgt1gYPvz/peUQvvpOm4JzIzGXPzluuutpnCBy75v5+oiwT3YRrLL/Meims9FtDDXL3qQubAE/ezIOOpm0N5XXV8DxIom8EN71yq5ab1tqhM+tBX7owRjy4FR4If2Q8feBdmTuh26DIQt/y+qSG8VkB9Sw/JCjc7c= - on: - tags: true - repo: pytest-dev/pytest-xdist From cf45eab9771ee271f8ec3eb4d33e23c914c70126 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 12 Dec 2020 12:02:17 -0300 Subject: [PATCH 006/113] Use GHA badge in README --- README.rst | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/README.rst b/README.rst index 61c16b54..ed93b7cd 100644 --- a/README.rst +++ b/README.rst @@ -11,13 +11,8 @@ :alt: Python versions :target: https://pypi.python.org/pypi/pytest-xdist -.. image:: https://travis-ci.org/pytest-dev/pytest-xdist.svg?branch=master - :alt: Travis CI build status - :target: https://travis-ci.org/pytest-dev/pytest-xdist - -.. image:: https://ci.appveyor.com/api/projects/status/56eq1a1avd4sdd7e/branch/master?svg=true - :alt: AppVeyor build status - :target: https://ci.appveyor.com/project/pytestbot/pytest-xdist +.. image:: https://github.com/pytest-dev/pytest-xdist/workflows/build/badge.svg + :target: https://github.com/pytest-dev/pytest-xdist/actions .. image:: https://img.shields.io/badge/code%20style-black-000000.svg :target: https://github.com/ambv/black From db896a9d337c503d64b5620a5d5087c17484b81e Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 12 Dec 2020 12:06:33 -0300 Subject: [PATCH 007/113] tox linting env expects Python 3.7 --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 4e273197..58bc29ca 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -37,7 +37,7 @@ jobs: - tox_env: "py38-psutil" python: "3.8" - tox_env: "linting" - python: "3.8" + python: "3.7" steps: - uses: actions/checkout@v1 From 16d636391b87bce76f84266cacb9df54869f3b13 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 12 Dec 2020 11:34:57 -0300 Subject: [PATCH 008/113] Propagate internal errors to the master node This should help users diagnose internal errors in workers like exceptions from hooks or in pytest itself. --- changelog/608.feature.rst | 1 + src/xdist/dsession.py | 18 ++++++++++++++++++ src/xdist/remote.py | 4 +++- src/xdist/workermanage.py | 2 ++ testing/acceptance_test.py | 12 ++++++++++++ 5 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 changelog/608.feature.rst diff --git a/changelog/608.feature.rst b/changelog/608.feature.rst new file mode 100644 index 00000000..130cc114 --- /dev/null +++ b/changelog/608.feature.rst @@ -0,0 +1 @@ +Internal errors in workers are now propagated to the master node. diff --git a/src/xdist/dsession.py b/src/xdist/dsession.py index 07ef091e..3db8e025 100644 --- a/src/xdist/dsession.py +++ b/src/xdist/dsession.py @@ -174,6 +174,24 @@ def worker_workerfinished(self, node): assert not crashitem, (crashitem, node) self._active_nodes.remove(node) + def worker_internal_error(self, node, formatted_error): + """ + pytest_internalerror() was called on the worker. + + pytest_internalerror() arguments are an excinfo and an excrepr, which can't + be serialized, so we go with a poor man's solution of raising an exception + here ourselves using the formatted message. + """ + self._active_nodes.remove(node) + try: + assert False, formatted_error + except AssertionError: + from _pytest._code import ExceptionInfo + + excinfo = ExceptionInfo.from_current() + excrepr = excinfo.getrepr() + self.config.hook.pytest_internalerror(excrepr=excrepr, excinfo=excinfo) + def worker_errordown(self, node, error): """Emitted by the WorkerController when a node dies.""" self.config.hook.pytest_testnodedown(node=node, error=error) diff --git a/src/xdist/remote.py b/src/xdist/remote.py index 97dc180c..aaa45bed 100644 --- a/src/xdist/remote.py +++ b/src/xdist/remote.py @@ -33,8 +33,10 @@ def sendevent(self, name, **kwargs): self.channel.send((name, kwargs)) def pytest_internalerror(self, excrepr): - for line in str(excrepr).split("\n"): + formatted_error = str(excrepr) + for line in formatted_error.split("\n"): self.log("IERROR>", line) + interactor.sendevent("internal_error", formatted_error=formatted_error) def pytest_sessionstart(self, session): self.session = session diff --git a/src/xdist/workermanage.py b/src/xdist/workermanage.py index dfcb59b8..6a705d34 100644 --- a/src/xdist/workermanage.py +++ b/src/xdist/workermanage.py @@ -324,6 +324,8 @@ def process_from_remote(self, eventcall): # noqa too complex self.log("ignoring {}({})".format(eventname, kwargs)) elif eventname == "workerready": self.notify_inproc(eventname, node=self, **kwargs) + elif eventname == "internal_error": + self.notify_inproc(eventname, node=self, **kwargs) elif eventname == "workerfinished": self._down = True self.workeroutput = kwargs["workeroutput"] diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index b71d1d94..c273bfac 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -1138,6 +1138,18 @@ def test_aaa1(crasher): assert "INTERNALERROR" not in result.stderr.str() +def test_internal_errors_propagate_to_master(testdir): + testdir.makeconftest( + """ + def pytest_collection_modifyitems(): + raise RuntimeError("Some runtime error") + """ + ) + testdir.makepyfile("def test(): pass") + result = testdir.runpytest("-n1") + result.stdout.fnmatch_lines(["*RuntimeError: Some runtime error*"]) + + class TestLoadScope: def test_by_module(self, testdir): test_file = """ From 8f90fb73db1309bba62e3a5b606be57632dc73f9 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 14 Dec 2020 09:07:15 -0300 Subject: [PATCH 009/113] Release 2.2.0 --- CHANGELOG.rst | 9 +++++++++ changelog/608.feature.rst | 1 - 2 files changed, 9 insertions(+), 1 deletion(-) delete mode 100644 changelog/608.feature.rst diff --git a/CHANGELOG.rst b/CHANGELOG.rst index ad758a30..7d38ef8f 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,3 +1,12 @@ +pytest-xdist 2.2.0 (2020-12-14) +=============================== + +Features +-------- + +- `#608 `_: Internal errors in workers are now propagated to the master node. + + pytest-xdist 2.1.0 (2020-08-25) =============================== diff --git a/changelog/608.feature.rst b/changelog/608.feature.rst deleted file mode 100644 index 130cc114..00000000 --- a/changelog/608.feature.rst +++ /dev/null @@ -1 +0,0 @@ -Internal errors in workers are now propagated to the master node. From ad99d943de4b0f14511dad48ee148f6ab8abacd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= Date: Tue, 15 Dec 2020 13:07:13 +0100 Subject: [PATCH 010/113] Disable pytest-services plugin in test to avoid worker_id conflict (#612) Co-authored-by: Bruno Oliveira --- tox.ini | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index af7aa4f3..dfa7860f 100644 --- a/tox.ini +++ b/tox.ini @@ -42,7 +42,9 @@ commands = towncrier --version {posargs} --yes [pytest] -addopts = -ra +# pytest-services also defines a worker_id fixture, disable +# it so they don't conflict with each other (#611). +addopts = -ra -p no:pytest-services testpaths = testing [flake8] From 40fa7b091dbc66a34c6e4155476752ca4ca920c3 Mon Sep 17 00:00:00 2001 From: staticdev Date: Tue, 19 Jan 2021 20:39:44 +0100 Subject: [PATCH 011/113] Add python 3.9 to classifiers --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index b6fb0f16..0bc80f09 100644 --- a/setup.py +++ b/setup.py @@ -44,5 +44,6 @@ "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", ], ) From aa89c42ad7d344a979d44c83bd3089931b09a9d3 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Sun, 7 Feb 2021 20:37:40 +0100 Subject: [PATCH 012/113] prepare for Node.fspath deprecation calculate topdir based on config.rootpath/rootdir addresses pytest-dev/pytest#8251 --- changelog/623.bugfix.rst | 1 + src/xdist/remote.py | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 changelog/623.bugfix.rst diff --git a/changelog/623.bugfix.rst b/changelog/623.bugfix.rst new file mode 100644 index 00000000..13397f9f --- /dev/null +++ b/changelog/623.bugfix.rst @@ -0,0 +1 @@ +Gracefully handle the pending deprecation of Node.fspath by using config.rootpath for topdir. diff --git a/src/xdist/remote.py b/src/xdist/remote.py index aaa45bed..9aaf91b9 100644 --- a/src/xdist/remote.py +++ b/src/xdist/remote.py @@ -93,9 +93,14 @@ def run_one_test(self, torun): ) def pytest_collection_finish(self, session): + try: + topdir = str(self.config.rootpath) + except AttributeError: # pytest <= 6.1.0 + topdir = str(self.config.rootdir) + self.sendevent( "collectionfinish", - topdir=str(session.fspath), + topdir=topdir, ids=[item.nodeid for item in session.items], ) From 166bdb410375dd082285ed57755c5250b3aa31aa Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Sun, 7 Feb 2021 22:07:12 +0100 Subject: [PATCH 013/113] prepare #592: replace master with controller where we can --- README.rst | 15 +++++++++++++-- changelog/592.trivial.rst | 1 + src/xdist/__init__.py | 15 +++++++++++++-- src/xdist/dsession.py | 4 ++-- src/xdist/newhooks.py | 2 +- src/xdist/plugin.py | 12 +++++++++--- src/xdist/remote.py | 4 ++-- testing/acceptance_test.py | 24 +++++++++++++++--------- 8 files changed, 56 insertions(+), 21 deletions(-) create mode 100644 changelog/592.trivial.rst diff --git a/README.rst b/README.rst index ed93b7cd..ae3097b2 100644 --- a/README.rst +++ b/README.rst @@ -285,7 +285,18 @@ Since version 2.0, the following functions are also available in the ``xdist`` m """ def is_xdist_master(request_or_session) -> bool: - """Return `True` if this is the xdist master, `False` otherwise + """Return `True` if this is the xdist controller, `False` otherwise + + Note: this method also returns `False` when distribution has not been + activated at all. + + deprecated alias for is_xdist_controller + + :param request_or_session: the `pytest` `request` or `session` object + """ + + def is_xdist_controller(request_or_session) -> bool: + """Return `True` if this is the xdist controller, `False` otherwise Note: this method also returns `False` when distribution has not been activated at all. @@ -295,7 +306,7 @@ Since version 2.0, the following functions are also available in the ``xdist`` m def get_xdist_worker_id(request_or_session) -> str: """Return the id of the current worker ('gw0', 'gw1', etc) or 'master' - if running on the 'master' node. + if running on the controller node. If not distributing tests (for example passing `-n0` or not passing `-n` at all) also return 'master'. diff --git a/changelog/592.trivial.rst b/changelog/592.trivial.rst new file mode 100644 index 00000000..a240729e --- /dev/null +++ b/changelog/592.trivial.rst @@ -0,0 +1 @@ +Replace master with controller where ever possible. diff --git a/src/xdist/__init__.py b/src/xdist/__init__.py index 83ef7762..031a3d34 100644 --- a/src/xdist/__init__.py +++ b/src/xdist/__init__.py @@ -1,4 +1,15 @@ -from xdist.plugin import is_xdist_worker, is_xdist_master, get_xdist_worker_id +from xdist.plugin import ( + is_xdist_worker, + is_xdist_master, + get_xdist_worker_id, + is_xdist_controller, +) from xdist._version import version as __version__ -__all__ = ["__version__", "is_xdist_worker", "is_xdist_master", "get_xdist_worker_id"] +__all__ = [ + "__version__", + "is_xdist_worker", + "is_xdist_master", + "is_xdist_controller", + "get_xdist_worker_id", +] diff --git a/src/xdist/dsession.py b/src/xdist/dsession.py index 3db8e025..ab927fa2 100644 --- a/src/xdist/dsession.py +++ b/src/xdist/dsession.py @@ -87,7 +87,7 @@ def pytest_sessionfinish(self, session): self._session = None def pytest_collection(self): - # prohibit collection of test items in master process + # prohibit collection of test items in controller process return True @pytest.mark.trylast @@ -240,7 +240,7 @@ def worker_collectionfinish(self, node, ids): return self.config.hook.pytest_xdist_node_collection_finished(node=node, ids=ids) # tell session which items were effectively collected otherwise - # the master node will finish the session with EXIT_NOTESTSCOLLECTED + # the controller node will finish the session with EXIT_NOTESTSCOLLECTED self._session.testscollected = len(ids) self.sched.add_node_collection(node, ids) if self.terminal: diff --git a/src/xdist/newhooks.py b/src/xdist/newhooks.py index 4ac71960..0e2efe9b 100644 --- a/src/xdist/newhooks.py +++ b/src/xdist/newhooks.py @@ -48,7 +48,7 @@ def pytest_testnodedown(node, error): def pytest_xdist_node_collection_finished(node, ids): - """called by the master node when a node finishes collecting. + """called by the controller node when a worker node finishes collecting. """ diff --git a/src/xdist/plugin.py b/src/xdist/plugin.py index 2d8424d9..9e7317fc 100644 --- a/src/xdist/plugin.py +++ b/src/xdist/plugin.py @@ -227,8 +227,8 @@ def is_xdist_worker(request_or_session) -> bool: return hasattr(request_or_session.config, "workerinput") -def is_xdist_master(request_or_session) -> bool: - """Return `True` if this is the xdist master, `False` otherwise +def is_xdist_controller(request_or_session) -> bool: + """Return `True` if this is the xdist controller, `False` otherwise Note: this method also returns `False` when distribution has not been activated at all. @@ -241,9 +241,13 @@ def is_xdist_master(request_or_session) -> bool: ) +# ALIAS: todo, deprecate +is_xdist_master = is_xdist_controller + + def get_xdist_worker_id(request_or_session) -> str: """Return the id of the current worker ('gw0', 'gw1', etc) or 'master' - if running on the 'master' node. + if running on the controller node. If not distributing tests (for example passing `-n0` or not passing `-n` at all) also return 'master'. @@ -253,6 +257,7 @@ def get_xdist_worker_id(request_or_session) -> str: if hasattr(request_or_session.config, "workerinput"): return request_or_session.config.workerinput["workerid"] else: + # TODO: remove "master", ideally for a None return "master" @@ -261,6 +266,7 @@ def worker_id(request): """Return the id of the current worker ('gw0', 'gw1', etc) or 'master' if running on the master node. """ + # TODO: remove "master", ideally for a None return get_xdist_worker_id(request) diff --git a/src/xdist/remote.py b/src/xdist/remote.py index aaa45bed..a61c5c36 100644 --- a/src/xdist/remote.py +++ b/src/xdist/remote.py @@ -116,7 +116,7 @@ def pytest_runtest_logreport(self, report): self.sendevent("testreport", data=data) def pytest_collectreport(self, report): - # send only reports that have not passed to master as optimization (#330) + # send only reports that have not passed to controller as optimization (#330) if not report.passed: data = self.config.hook.pytest_report_to_serializable( config=self.config, report=report @@ -139,7 +139,7 @@ def serialize_warning_message(warning_message): message_class_name = type(warning_message.message).__name__ message_str = str(warning_message.message) # check now if we can serialize the warning arguments (#349) - # if not, we will just use the exception message on the master node + # if not, we will just use the exception message on the controller node try: dumps(warning_message.message.args) except DumpError: diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index c273bfac..382c9381 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -244,7 +244,7 @@ def pytest_load_initial_conftests(early_config): def test_data_exchange(self, testdir): testdir.makeconftest( """ - # This hook only called on master. + # This hook only called on the controlling process. def pytest_configure_node(node): node.workerinput['a'] = 42 node.workerinput['b'] = 7 @@ -257,7 +257,7 @@ def pytest_configure(config): r = a + b config.workeroutput['r'] = r - # This hook only called on master. + # This hook only called on the controlling process. def pytest_testnodedown(node, error): node.config.calc_result = node.workeroutput['r'] @@ -289,7 +289,7 @@ def pytest_sessionfinish(session): # on the worker if hasattr(session.config, 'workeroutput'): session.config.workeroutput['s2'] = 42 - # on the master + # on the controller def pytest_testnodedown(node, error): assert node.workeroutput['s2'] == 42 print ("s2call-finished") @@ -503,7 +503,7 @@ def pytest_sessionfinish(session): if hasattr(session.config, 'workerinput'): name = "worker" else: - name = "master" + name = "controller" with open(name, "w") as f: f.write("xy") # let's fail on the worker @@ -524,12 +524,12 @@ def test_hello(): d = result.parseoutcomes() assert d["passed"] == 1 assert testdir.tmpdir.join("worker").check() - assert testdir.tmpdir.join("master").check() + assert testdir.tmpdir.join("controller").check() def test_session_testscollected(testdir): """ - Make sure master node is updating the session object with the number + Make sure controller node is updating the session object with the number of tests collected from the workers. """ testdir.makepyfile( @@ -574,7 +574,7 @@ def test_hello(myarg): def test_config_initialization(testdir, monkeypatch, pytestconfig): - """Ensure workers and master are initialized consistently. Integration test for #445""" + """Ensure workers and controller are initialized consistently. Integration test for #445""" testdir.makepyfile( **{ "dir_a/test_foo.py": """ @@ -1138,7 +1138,7 @@ def test_aaa1(crasher): assert "INTERNALERROR" not in result.stderr.str() -def test_internal_errors_propagate_to_master(testdir): +def test_internal_errors_propagate_to_controller(testdir): testdir.makeconftest( """ def pytest_collection_modifyitems(): @@ -1408,12 +1408,18 @@ def test_is_xdist_worker(self, fake_request): del fake_request.config.workerinput assert not xdist.is_xdist_worker(fake_request) - def test_is_xdist_master(self, fake_request): + def test_is_xdist_controller(self, fake_request): + assert not xdist.is_xdist_master(fake_request) + assert not xdist.is_xdist_controller(fake_request) + del fake_request.config.workerinput assert xdist.is_xdist_master(fake_request) + assert xdist.is_xdist_controller(fake_request) + fake_request.config.option.dist = "no" assert not xdist.is_xdist_master(fake_request) + assert not xdist.is_xdist_controller(fake_request) def test_get_xdist_worker_id(self, fake_request): assert xdist.get_xdist_worker_id(fake_request) == "gw5" From 9e4e8b74e019958733a6f0ec0b6c18b7d6e37325 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Tue, 9 Feb 2021 21:51:30 +0100 Subject: [PATCH 014/113] Apply suggestions from code review Co-authored-by: Bruno Oliveira --- src/xdist/plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/xdist/plugin.py b/src/xdist/plugin.py index 9e7317fc..1eba32b8 100644 --- a/src/xdist/plugin.py +++ b/src/xdist/plugin.py @@ -241,7 +241,7 @@ def is_xdist_controller(request_or_session) -> bool: ) -# ALIAS: todo, deprecate +# ALIAS: TODO, deprecate (#592) is_xdist_master = is_xdist_controller From b239199061e6273386b4a0e3e808ae2d5b965fbf Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Tue, 9 Feb 2021 21:55:43 +0100 Subject: [PATCH 015/113] release 2.2.1 --- CHANGELOG.rst | 9 +++++++++ changelog/623.bugfix.rst | 1 - 2 files changed, 9 insertions(+), 1 deletion(-) delete mode 100644 changelog/623.bugfix.rst diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 7d38ef8f..4e5bb6b0 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,3 +1,12 @@ +pytest-xdist 2.2.1 (2021-02-09) +=============================== + +Bug Fixes +--------- + +- `#623 `_: Gracefully handle the pending deprecation of Node.fspath by using config.rootpath for topdir. + + pytest-xdist 2.2.0 (2020-12-14) =============================== diff --git a/changelog/623.bugfix.rst b/changelog/623.bugfix.rst deleted file mode 100644 index 13397f9f..00000000 --- a/changelog/623.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Gracefully handle the pending deprecation of Node.fspath by using config.rootpath for topdir. From f5342962ab30bd8522e0af2ea1469b5149bde370 Mon Sep 17 00:00:00 2001 From: baekdohyeop Date: Mon, 15 Mar 2021 13:02:19 +0900 Subject: [PATCH 016/113] [HOTFIX] rename pytest's branch(master to main) --- changelog/638.bugfix.rst | 1 + tox.ini | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 changelog/638.bugfix.rst diff --git a/changelog/638.bugfix.rst b/changelog/638.bugfix.rst new file mode 100644 index 00000000..7fb978cb --- /dev/null +++ b/changelog/638.bugfix.rst @@ -0,0 +1 @@ +Fix issue caused by changing the branch name of the pytest repository. diff --git a/tox.ini b/tox.ini index dfa7860f..9dc2b848 100644 --- a/tox.ini +++ b/tox.ini @@ -9,7 +9,7 @@ envlist= extras = testing deps = pytestlatest: pytest - pytestmaster: git+https://github.com/pytest-dev/pytest.git@master + pytestmaster: git+https://github.com/pytest-dev/pytest.git@main commands= pytest {posargs} From f02aa70c955c570d76fbd807db718eba99a744df Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Fri, 19 Mar 2021 15:09:49 +0000 Subject: [PATCH 017/113] Upgrade pre-commit hooks Use `pre-commit autoupdate` to upgrade, and follow the message from `pre-commit-hooks` to move to the official `flake8` hook. --- .pre-commit-config.yaml | 9 ++++--- src/xdist/newhooks.py | 3 +-- src/xdist/workermanage.py | 13 +++++---- testing/acceptance_test.py | 54 ++++++++++++++++++++++++-------------- testing/test_newhooks.py | 3 +-- testing/test_remote.py | 4 ++- 6 files changed, 51 insertions(+), 35 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f48f7ce4..82fc078b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,20 +1,23 @@ repos: - repo: https://github.com/ambv/black - rev: 19.10b0 + rev: 20.8b1 hooks: - id: black args: [--safe, --quiet, --target-version, py35] language_version: python3.7 - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v2.4.0 + rev: v3.4.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer - id: check-yaml - id: debug-statements +- repo: https://gitlab.com/pycqa/flake8 + rev: 3.9.0 + hooks: - id: flake8 - repo: https://github.com/asottile/pyupgrade - rev: v2.7.2 + rev: v2.10.1 hooks: - id: pyupgrade args: [--py3-plus] diff --git a/src/xdist/newhooks.py b/src/xdist/newhooks.py index 0e2efe9b..da0f22ad 100644 --- a/src/xdist/newhooks.py +++ b/src/xdist/newhooks.py @@ -48,8 +48,7 @@ def pytest_testnodedown(node, error): def pytest_xdist_node_collection_finished(node, ids): - """called by the controller node when a worker node finishes collecting. - """ + """called by the controller node when a worker node finishes collecting.""" @pytest.mark.firstresult diff --git a/src/xdist/workermanage.py b/src/xdist/workermanage.py index 6a705d34..19ed73a0 100644 --- a/src/xdist/workermanage.py +++ b/src/xdist/workermanage.py @@ -157,8 +157,7 @@ def finished(): class HostRSync(execnet.RSync): - """ RSyncer that filters out common files - """ + """RSyncer that filters out common files""" def __init__(self, sourcedir, *args, **kwargs): self._synced = {} @@ -303,12 +302,12 @@ def notify_inproc(self, eventname, **kwargs): self.putevent((eventname, kwargs)) def process_from_remote(self, eventcall): # noqa too complex - """ this gets called for each object we receive from - the other side and if the channel closes. + """this gets called for each object we receive from + the other side and if the channel closes. - Note that channel callbacks run in the receiver - thread of execnet gateways - we need to - avoid raising exceptions or doing heavy work. + Note that channel callbacks run in the receiver + thread of execnet gateways - we need to + avoid raising exceptions or doing heavy work. """ try: if eventcall == self.ENDMARK: diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index 382c9381..f7f21bac 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -718,8 +718,7 @@ def test_foo(x): def test_tmpdir_disabled(testdir): - """Test xdist doesn't break if internal tmpdir plugin is disabled (#22). - """ + """Test xdist doesn't break if internal tmpdir plugin is disabled (#22).""" p1 = testdir.makepyfile( """ def test_ok(): @@ -733,8 +732,7 @@ def test_ok(): @pytest.mark.parametrize("plugin", ["xdist.looponfail", "xdist.boxed"]) def test_sub_plugins_disabled(testdir, plugin): - """Test that xdist doesn't break if we disable any of its sub-plugins. (#32) - """ + """Test that xdist doesn't break if we disable any of its sub-plugins. (#32)""" p1 = testdir.makepyfile( """ def test_ok(): @@ -1239,14 +1237,22 @@ def test(self, i): "test_b.py::TestB", result.outlines ) - assert test_a_workers_and_test_count in ( - {"gw0": 10}, - {"gw1": 0}, - ) or test_a_workers_and_test_count in ({"gw0": 0}, {"gw1": 10}) - assert test_b_workers_and_test_count in ( - {"gw0": 10}, - {"gw1": 0}, - ) or test_b_workers_and_test_count in ({"gw0": 0}, {"gw1": 10}) + assert ( + test_a_workers_and_test_count + in ( + {"gw0": 10}, + {"gw1": 0}, + ) + or test_a_workers_and_test_count in ({"gw0": 0}, {"gw1": 10}) + ) + assert ( + test_b_workers_and_test_count + in ( + {"gw0": 10}, + {"gw1": 0}, + ) + or test_b_workers_and_test_count in ({"gw0": 0}, {"gw1": 10}) + ) def test_by_class(self, testdir): testdir.makepyfile( @@ -1271,14 +1277,22 @@ def test(self, i): "test_a.py::TestB", result.outlines ) - assert test_a_workers_and_test_count in ( - {"gw0": 10}, - {"gw1": 0}, - ) or test_a_workers_and_test_count in ({"gw0": 0}, {"gw1": 10}) - assert test_b_workers_and_test_count in ( - {"gw0": 10}, - {"gw1": 0}, - ) or test_b_workers_and_test_count in ({"gw0": 0}, {"gw1": 10}) + assert ( + test_a_workers_and_test_count + in ( + {"gw0": 10}, + {"gw1": 0}, + ) + or test_a_workers_and_test_count in ({"gw0": 0}, {"gw1": 10}) + ) + assert ( + test_b_workers_and_test_count + in ( + {"gw0": 10}, + {"gw1": 0}, + ) + or test_b_workers_and_test_count in ({"gw0": 0}, {"gw1": 10}) + ) def test_module_single_start(self, testdir): """Fix test suite never finishing in case all workers start with a single test (#277).""" diff --git a/testing/test_newhooks.py b/testing/test_newhooks.py index 741e64fd..03184422 100644 --- a/testing/test_newhooks.py +++ b/testing/test_newhooks.py @@ -46,8 +46,7 @@ def pytest_runtest_logreport(report): ) def test_node_collection_finished(self, testdir): - """Test pytest_xdist_node_collection_finished hook (#8). - """ + """Test pytest_xdist_node_collection_finished hook (#8).""" testdir.makeconftest( """ def pytest_xdist_node_collection_finished(node, ids): diff --git a/testing/test_remote.py b/testing/test_remote.py index da2f6a86..2f6e2221 100644 --- a/testing/test_remote.py +++ b/testing/test_remote.py @@ -37,7 +37,9 @@ def __init__(self, request, testdir): self.testdir = testdir self.events = Queue() - def setup(self,): + def setup( + self, + ): self.testdir.chdir() # import os ; os.environ['EXECNET_DEBUG'] = "2" self.gateway = execnet.makegateway() From 1dc019709c15678faa622834d3bc851abddb426c Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Sat, 20 Mar 2021 14:43:57 +0000 Subject: [PATCH 018/113] Use 'main' to refer to pytest default branch in tox env names. (#643) Co-authored-by: Thomas Grainger Co-authored-by: Bruno Oliveira --- .github/workflows/main.yml | 4 ++-- changelog/643.trivial.rst | 1 + tox.ini | 4 ++-- 3 files changed, 5 insertions(+), 4 deletions(-) create mode 100644 changelog/643.trivial.rst diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 58bc29ca..8801b79f 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -16,7 +16,7 @@ jobs: - "py37-pytestlatest" - "py38-pytestlatest" - "py39-pytestlatest" - - "py38-pytestmaster" + - "py38-pytestmain" - "py38-psutil" - "linting" @@ -32,7 +32,7 @@ jobs: python: "3.8" - tox_env: "py39-pytestlatest" python: "3.9" - - tox_env: "py38-pytestmaster" + - tox_env: "py38-pytestmain" python: "3.8" - tox_env: "py38-psutil" python: "3.8" diff --git a/changelog/643.trivial.rst b/changelog/643.trivial.rst new file mode 100644 index 00000000..927d2627 --- /dev/null +++ b/changelog/643.trivial.rst @@ -0,0 +1 @@ +Use 'main' to refer to pytest default branch in tox env names. diff --git a/tox.ini b/tox.ini index 9dc2b848..78f3bedc 100644 --- a/tox.ini +++ b/tox.ini @@ -2,14 +2,14 @@ envlist= linting py{35,36,37,38,39}-pytestlatest - py38-pytestmaster + py38-pytestmain py38-psutil [testenv] extras = testing deps = pytestlatest: pytest - pytestmaster: git+https://github.com/pytest-dev/pytest.git@main + pytestmain: git+https://github.com/pytest-dev/pytest.git commands= pytest {posargs} From 9785a316ae62ebc042caee4115c04791a1ba4e92 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 7 Apr 2021 10:13:14 -0300 Subject: [PATCH 019/113] [pre-commit.ci] pre-commit autoupdate (#645) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 82fc078b..7cd4cf41 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,5 +1,5 @@ repos: -- repo: https://github.com/ambv/black +- repo: https://github.com/psf/black rev: 20.8b1 hooks: - id: black @@ -12,12 +12,12 @@ repos: - id: end-of-file-fixer - id: check-yaml - id: debug-statements -- repo: https://gitlab.com/pycqa/flake8 +- repo: https://github.com/PyCQA/flake8 rev: 3.9.0 hooks: - id: flake8 - repo: https://github.com/asottile/pyupgrade - rev: v2.10.1 + rev: v2.11.0 hooks: - id: pyupgrade args: [--py3-plus] From 04dde88996bb6554843b85cd9be9ace464004de3 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 19 Apr 2021 17:12:26 +0000 Subject: [PATCH 020/113] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/PyCQA/flake8: 3.9.0 → 3.9.1](https://github.com/PyCQA/flake8/compare/3.9.0...3.9.1) - [github.com/asottile/pyupgrade: v2.11.0 → v2.12.0](https://github.com/asottile/pyupgrade/compare/v2.11.0...v2.12.0) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7cd4cf41..dd8744a8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -13,11 +13,11 @@ repos: - id: check-yaml - id: debug-statements - repo: https://github.com/PyCQA/flake8 - rev: 3.9.0 + rev: 3.9.1 hooks: - id: flake8 - repo: https://github.com/asottile/pyupgrade - rev: v2.11.0 + rev: v2.12.0 hooks: - id: pyupgrade args: [--py3-plus] From 8b35945d54dd6bc5f775a0fb3f706f19b465b292 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sun, 25 Apr 2021 09:26:15 -0300 Subject: [PATCH 021/113] Drop Python 3.5 support pytest no longer supports it, and our CI is broken because of it. --- .github/workflows/main.yml | 26 +------------------------- README.rst | 10 +++++----- changelog/654.removal.rst | 1 + setup.py | 3 +-- tox.ini | 10 +--------- 5 files changed, 9 insertions(+), 41 deletions(-) create mode 100644 changelog/654.removal.rst diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 8801b79f..c9eb227b 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -11,19 +11,15 @@ jobs: fail-fast: false matrix: tox_env: - - "py35-pytestlatest" - "py36-pytestlatest" - "py37-pytestlatest" - "py38-pytestlatest" - "py39-pytestlatest" - "py38-pytestmain" - "py38-psutil" - - "linting" os: [ubuntu-latest, windows-latest] include: - - tox_env: "py35-pytestlatest" - python: "3.5" - tox_env: "py36-pytestlatest" python: "3.6" - tox_env: "py37-pytestlatest" @@ -36,8 +32,6 @@ jobs: python: "3.8" - tox_env: "py38-psutil" python: "3.8" - - tox_env: "linting" - python: "3.7" steps: - uses: actions/checkout@v1 @@ -53,31 +47,13 @@ jobs: run: | tox -e ${{ matrix.tox_env }} - linting: - - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v1 - - name: Set up Python - uses: actions/setup-python@v1 - with: - python-version: "3.7" - - name: Install tox - run: | - python -m pip install --upgrade pip - pip install tox - - name: Linting - run: | - tox -e linting - deploy: if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') runs-on: ubuntu-latest - needs: [build, linting] + needs: build steps: - uses: actions/checkout@v1 diff --git a/README.rst b/README.rst index ae3097b2..da5daaf2 100644 --- a/README.rst +++ b/README.rst @@ -149,16 +149,16 @@ where executing a high-scope fixture exactly once is important. Running tests in a Python subprocess ------------------------------------ -To instantiate a python3.5 subprocess and send tests to it, you may type:: +To instantiate a python3.9 subprocess and send tests to it, you may type:: - pytest -d --tx popen//python=python3.5 + pytest -d --tx popen//python=python3.9 -This will start a subprocess which is run with the ``python3.5`` +This will start a subprocess which is run with the ``python3.9`` Python interpreter, found in your system binary lookup path. If you prefix the --tx option value like this:: - --tx 3*popen//python=python3.5 + --tx 3*popen//python=python3.9 then three subprocesses would be created and tests will be load-balanced across these three processes. @@ -371,7 +371,7 @@ You can also add default environments like this: .. code-block:: ini [pytest] - addopts = --tx ssh=myhost//python=python3.5 --tx ssh=myhost//python=python3.6 + addopts = --tx ssh=myhost//python=python3.9 --tx ssh=myhost//python=python3.6 and then just type:: diff --git a/changelog/654.removal.rst b/changelog/654.removal.rst new file mode 100644 index 00000000..b2159e35 --- /dev/null +++ b/changelog/654.removal.rst @@ -0,0 +1 @@ +Python 3.5 is no longer supported. diff --git a/setup.py b/setup.py index 0bc80f09..9a75ca52 100644 --- a/setup.py +++ b/setup.py @@ -23,7 +23,7 @@ "pytest11": ["xdist = xdist.plugin", "xdist.looponfail = xdist.looponfail"] }, zip_safe=False, - python_requires=">=3.5", + python_requires=">=3.6", install_requires=install_requires, setup_requires=["setuptools_scm"], classifiers=[ @@ -40,7 +40,6 @@ "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", diff --git a/tox.ini b/tox.ini index 78f3bedc..0e4ad977 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,7 @@ [tox] envlist= linting - py{35,36,37,38,39}-pytestlatest + py{36,37,38,39}-pytestlatest py38-pytestmain py38-psutil @@ -21,14 +21,6 @@ deps = pytest commands = pytest {posargs:-k psutil} -[testenv:linting] -skip_install = True -usedevelop = True -passenv = PRE_COMMIT_HOME -deps = - pre-commit -commands = pre-commit run --all-files --show-diff-on-failure - [testenv:release] changedir= decription = do a release, required posarg of the version number From f861f1667db0a09fd6c303e54593aafed6ce9147 Mon Sep 17 00:00:00 2001 From: xoviat <49173759+xoviat@users.noreply.github.com> Date: Tue, 27 Apr 2021 07:07:49 -0500 Subject: [PATCH 022/113] add new pytest_handlecrashitem hook allows handling and rescheduling crash tests PR #651 Co-authored-by: xoviat Co-authored-by: Bruno Oliveira --- changelog/650.feature.rst | 1 + src/xdist/dsession.py | 6 ++++++ src/xdist/newhooks.py | 17 ++++++++++++++++ src/xdist/scheduler/each.py | 8 ++++++++ src/xdist/scheduler/load.py | 8 ++++++++ src/xdist/scheduler/loadscope.py | 3 +++ testing/test_newhooks.py | 34 ++++++++++++++++++++++++++++++++ 7 files changed, 77 insertions(+) create mode 100644 changelog/650.feature.rst diff --git a/changelog/650.feature.rst b/changelog/650.feature.rst new file mode 100644 index 00000000..ce6dc399 --- /dev/null +++ b/changelog/650.feature.rst @@ -0,0 +1 @@ +Added new ``pytest_handlecrashitem`` hook to allow handling and rescheduling crashed items. diff --git a/src/xdist/dsession.py b/src/xdist/dsession.py index ab927fa2..ccd5e209 100644 --- a/src/xdist/dsession.py +++ b/src/xdist/dsession.py @@ -343,6 +343,12 @@ def handle_crashitem(self, nodeid, worker): nodeid, (fspath, None, fspath), (), "failed", msg, "???" ) rep.node = worker + + self.config.hook.pytest_handlecrashitem( + crashitem=nodeid, + report=rep, + sched=self.sched, + ) self.config.hook.pytest_runtest_logreport(report=rep) diff --git a/src/xdist/newhooks.py b/src/xdist/newhooks.py index da0f22ad..a6443f39 100644 --- a/src/xdist/newhooks.py +++ b/src/xdist/newhooks.py @@ -64,3 +64,20 @@ def pytest_xdist_auto_num_workers(config): .. versionadded:: 2.1 """ + + +@pytest.mark.firstresult +def pytest_handlecrashitem(crashitem, report, sched): + """ + Handle a crashitem, modifying the report if necessary. + + The scheduler is provided as a parameter to reschedule the test if desired with + `sched.mark_test_pending`. + + def pytest_handlecrashitem(crashitem, report, sched): + if should_rerun(crashitem): + sched.mark_test_pending(crashitem) + report.outcome = "rerun" + + .. versionadded:: 2.2.1 + """ diff --git a/src/xdist/scheduler/each.py b/src/xdist/scheduler/each.py index b2a04420..cfe99e7d 100644 --- a/src/xdist/scheduler/each.py +++ b/src/xdist/scheduler/each.py @@ -101,6 +101,14 @@ def add_node_collection(self, node, collection): def mark_test_complete(self, node, item_index, duration=0): self.node2pending[node].remove(item_index) + def mark_test_pending(self, item): + self.pending.insert( + 0, + self.collection.index(item), + ) + for node in self.node2pending: + self.check_schedule(node) + def remove_node(self, node): # KeyError if we didn't get an add_node() yet pending = self.node2pending.pop(node) diff --git a/src/xdist/scheduler/load.py b/src/xdist/scheduler/load.py index e378d9a6..f32caa55 100644 --- a/src/xdist/scheduler/load.py +++ b/src/xdist/scheduler/load.py @@ -151,6 +151,14 @@ def mark_test_complete(self, node, item_index, duration=0): self.node2pending[node].remove(item_index) self.check_schedule(node, duration=duration) + def mark_test_pending(self, item): + self.pending.insert( + 0, + self.collection.index(item), + ) + for node in self.node2pending: + self.check_schedule(node) + def check_schedule(self, node, duration=0): """Maybe schedule new items on the node diff --git a/src/xdist/scheduler/loadscope.py b/src/xdist/scheduler/loadscope.py index 31dbe26c..c25e4769 100644 --- a/src/xdist/scheduler/loadscope.py +++ b/src/xdist/scheduler/loadscope.py @@ -243,6 +243,9 @@ def mark_test_complete(self, node, item_index, duration=0): self.assigned_work[node][scope][nodeid] = True self._reschedule(node) + def mark_test_pending(self, item): + raise NotImplementedError() + def _assign_work_unit(self, node): """Assign a work unit to a node.""" assert self.workqueue diff --git a/testing/test_newhooks.py b/testing/test_newhooks.py index 03184422..d2c28788 100644 --- a/testing/test_newhooks.py +++ b/testing/test_newhooks.py @@ -60,3 +60,37 @@ def pytest_xdist_node_collection_finished(node, ids): ["*HOOK: gw0 test_a, test_b, test_c", "*HOOK: gw1 test_a, test_b, test_c"] ) res.stdout.fnmatch_lines(["*3 passed*"]) + + +class TestCrashItem: + @pytest.fixture(autouse=True) + def create_test_file(self, testdir): + testdir.makepyfile( + """ + import os + def test_a(): pass + def test_b(): os._exit(1) + def test_c(): pass + def test_d(): pass + """ + ) + + def test_handlecrashitem(self, testdir): + """Test pytest_handlecrashitem hook.""" + testdir.makeconftest( + """ + test_runs = 0 + + def pytest_handlecrashitem(crashitem, report, sched): + global test_runs + + if test_runs == 0: + sched.mark_test_pending(crashitem) + test_runs = 1 + else: + print("HOOK: pytest_handlecrashitem") + """ + ) + res = testdir.runpytest("-n2", "-s") + res.stdout.fnmatch_lines_random(["*HOOK: pytest_handlecrashitem"]) + res.stdout.fnmatch_lines(["*3 passed*"]) From 260211603eaf4e4eb1ee80dc8e2e7060203fafe3 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 27 Apr 2021 09:30:37 -0300 Subject: [PATCH 023/113] [pre-commit.ci] pre-commit autoupdate (#655) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 4 ++-- src/xdist/dsession.py | 2 +- src/xdist/newhooks.py | 18 +++++++++--------- src/xdist/workermanage.py | 2 +- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index dd8744a8..2b65e8a4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/psf/black - rev: 20.8b1 + rev: 21.4b0 hooks: - id: black args: [--safe, --quiet, --target-version, py35] @@ -17,7 +17,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/asottile/pyupgrade - rev: v2.12.0 + rev: v2.13.0 hooks: - id: pyupgrade args: [--py3-plus] diff --git a/src/xdist/dsession.py b/src/xdist/dsession.py index ccd5e209..d517a8a0 100644 --- a/src/xdist/dsession.py +++ b/src/xdist/dsession.py @@ -14,7 +14,7 @@ class Interrupted(KeyboardInterrupt): - """ signals an immediate interruption. """ + """signals an immediate interruption.""" class DSession: diff --git a/src/xdist/newhooks.py b/src/xdist/newhooks.py index a6443f39..d2482aff 100644 --- a/src/xdist/newhooks.py +++ b/src/xdist/newhooks.py @@ -15,36 +15,36 @@ def pytest_xdist_setupnodes(config, specs): - """ called before any remote node is set up. """ + """called before any remote node is set up.""" def pytest_xdist_newgateway(gateway): - """ called on new raw gateway creation. """ + """called on new raw gateway creation.""" def pytest_xdist_rsyncstart(source, gateways): - """ called before rsyncing a directory to remote gateways takes place. """ + """called before rsyncing a directory to remote gateways takes place.""" def pytest_xdist_rsyncfinish(source, gateways): - """ called after rsyncing a directory to remote gateways takes place. """ + """called after rsyncing a directory to remote gateways takes place.""" @pytest.mark.firstresult def pytest_xdist_getremotemodule(): - """ called when creating remote node""" + """called when creating remote node""" def pytest_configure_node(node): - """ configure node information before it gets instantiated. """ + """configure node information before it gets instantiated.""" def pytest_testnodeready(node): - """ Test Node is ready to operate. """ + """Test Node is ready to operate.""" def pytest_testnodedown(node, error): - """ Test Node is down. """ + """Test Node is down.""" def pytest_xdist_node_collection_finished(node, ids): @@ -53,7 +53,7 @@ def pytest_xdist_node_collection_finished(node, ids): @pytest.mark.firstresult def pytest_xdist_make_scheduler(config, log): - """ return a node scheduler implementation """ + """return a node scheduler implementation""" @pytest.mark.firstresult diff --git a/src/xdist/workermanage.py b/src/xdist/workermanage.py index 19ed73a0..8fed077e 100644 --- a/src/xdist/workermanage.py +++ b/src/xdist/workermanage.py @@ -293,7 +293,7 @@ def shutdown(self): self._shutdown_sent = True def sendcommand(self, name, **kwargs): - """ send a named parametrized command to the other side. """ + """send a named parametrized command to the other side.""" self.log("sending command {}(**{})".format(name, kwargs)) self.channel.send((name, kwargs)) From 4d27110bebfaa73fc3c24671a277056e896a6bdf Mon Sep 17 00:00:00 2001 From: Kyle Roeschley Date: Thu, 25 Mar 2021 10:37:29 -0500 Subject: [PATCH 024/113] Add 'logical' CPU count choice for --numprocesses This can significantly speed up runs when testing is not CPU-bound. --- changelog/646.feature.rst | 3 +++ src/xdist/plugin.py | 18 ++++++++++-------- testing/test_plugin.py | 16 +++++++++++++++- 3 files changed, 28 insertions(+), 9 deletions(-) create mode 100644 changelog/646.feature.rst diff --git a/changelog/646.feature.rst b/changelog/646.feature.rst new file mode 100644 index 00000000..210c73ee --- /dev/null +++ b/changelog/646.feature.rst @@ -0,0 +1,3 @@ +Add ``--numprocesses=logical`` flag, which automatically uses the number of logical CPUs available, instead of physical CPUs with ``auto``. + +This is very useful for test suites which are not CPU-bound. diff --git a/src/xdist/plugin.py b/src/xdist/plugin.py index 1eba32b8..16ecf373 100644 --- a/src/xdist/plugin.py +++ b/src/xdist/plugin.py @@ -5,13 +5,14 @@ import pytest -def pytest_xdist_auto_num_workers(): +def pytest_xdist_auto_num_workers(config): try: import psutil except ImportError: pass else: - count = psutil.cpu_count(logical=False) or psutil.cpu_count() + use_logical = config.option.numprocesses == "logical" + count = psutil.cpu_count(logical=use_logical) or psutil.cpu_count() if count: return count try: @@ -36,8 +37,8 @@ def cpu_count(): def parse_numprocesses(s): - if s == "auto": - return "auto" + if s == "auto" or s == "logical": + return s elif s is not None: return int(s) @@ -51,9 +52,10 @@ def pytest_addoption(parser): metavar="numprocesses", action="store", type=parse_numprocesses, - help="shortcut for '--dist=load --tx=NUM*popen', " - "you can use 'auto' here for auto detection CPUs number on " - "host system and it will be 0 when used with --pdb", + help="Shortcut for '--dist=load --tx=NUM*popen'. With 'auto', attempt " + "to detect physical CPU count. With 'logical', detect logical CPU " + "count. If physical CPU count cannot be found, falls back to logical " + "count. This will be 0 when used with --pdb.", ) group.addoption( "--maxprocesses", @@ -190,7 +192,7 @@ def pytest_configure(config): @pytest.mark.tryfirst def pytest_cmdline_main(config): usepdb = config.getoption("usepdb", False) # a core option - if config.option.numprocesses == "auto": + if config.option.numprocesses == "auto" or config.option.numprocesses == "logical": if usepdb: config.option.numprocesses = 0 config.option.dist = "no" diff --git a/testing/test_plugin.py b/testing/test_plugin.py index c1aac652..58706767 100644 --- a/testing/test_plugin.py +++ b/testing/test_plugin.py @@ -69,6 +69,12 @@ def test_auto_detect_cpus(testdir, monkeypatch): assert config.getoption("numprocesses") == 0 assert config.getoption("dist") == "no" + config = testdir.parseconfigure("-nlogical", "--pdb") + check_options(config) + assert config.getoption("usepdb") + assert config.getoption("numprocesses") == 0 + assert config.getoption("dist") == "no" + monkeypatch.delattr(os, "sched_getaffinity", raising=False) monkeypatch.setenv("TRAVIS", "true") config = testdir.parseconfigure("-nauto") @@ -81,12 +87,16 @@ def test_auto_detect_cpus_psutil(testdir, monkeypatch): psutil = pytest.importorskip("psutil") - monkeypatch.setattr(psutil, "cpu_count", lambda logical=True: 42) + monkeypatch.setattr(psutil, "cpu_count", lambda logical=True: 84 if logical else 42) config = testdir.parseconfigure("-nauto") check_options(config) assert config.getoption("numprocesses") == 42 + config = testdir.parseconfigure("-nlogical") + check_options(config) + assert config.getoption("numprocesses") == 84 + def test_hook_auto_num_workers(testdir, monkeypatch): from xdist.plugin import pytest_cmdline_main as check_options @@ -101,6 +111,10 @@ def pytest_xdist_auto_num_workers(): check_options(config) assert config.getoption("numprocesses") == 42 + config = testdir.parseconfigure("-nlogical") + check_options(config) + assert config.getoption("numprocesses") == 42 + def test_boxed_with_collect_only(testdir): from xdist.plugin import pytest_cmdline_main as check_options From d4d01bba2e5e387e5ae08c0bae625754ba0d67b1 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 27 Apr 2021 11:51:04 -0300 Subject: [PATCH 025/113] Apply suggestions from code review --- src/xdist/plugin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/xdist/plugin.py b/src/xdist/plugin.py index 16ecf373..f7d1ff8e 100644 --- a/src/xdist/plugin.py +++ b/src/xdist/plugin.py @@ -37,7 +37,7 @@ def cpu_count(): def parse_numprocesses(s): - if s == "auto" or s == "logical": + if s in ("auto", "logical"): return s elif s is not None: return int(s) @@ -192,7 +192,7 @@ def pytest_configure(config): @pytest.mark.tryfirst def pytest_cmdline_main(config): usepdb = config.getoption("usepdb", False) # a core option - if config.option.numprocesses == "auto" or config.option.numprocesses == "logical": + if config.option.numprocesses in ("auto", "logical"): if usepdb: config.option.numprocesses = 0 config.option.dist = "no" From 3a7604bdb739032990de0c77d03e395bcf7a5491 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 3 May 2021 17:21:44 +0000 Subject: [PATCH 026/113] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/psf/black: 21.4b0 → 21.4b2](https://github.com/psf/black/compare/21.4b0...21.4b2) - [github.com/asottile/pyupgrade: v2.13.0 → v2.14.0](https://github.com/asottile/pyupgrade/compare/v2.13.0...v2.14.0) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2b65e8a4..07a07bce 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/psf/black - rev: 21.4b0 + rev: 21.4b2 hooks: - id: black args: [--safe, --quiet, --target-version, py35] @@ -17,7 +17,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/asottile/pyupgrade - rev: v2.13.0 + rev: v2.14.0 hooks: - id: pyupgrade args: [--py3-plus] From cc828ddbfc72e843b69a8a4b9c218ddc6d7991ae Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 10 May 2021 20:26:00 +0000 Subject: [PATCH 027/113] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/psf/black: 21.4b2 → 21.5b1](https://github.com/psf/black/compare/21.4b2...21.5b1) - [github.com/PyCQA/flake8: 3.9.1 → 3.9.2](https://github.com/PyCQA/flake8/compare/3.9.1...3.9.2) - [github.com/asottile/pyupgrade: v2.14.0 → v2.15.0](https://github.com/asottile/pyupgrade/compare/v2.14.0...v2.15.0) --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 07a07bce..effa004a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/psf/black - rev: 21.4b2 + rev: 21.5b1 hooks: - id: black args: [--safe, --quiet, --target-version, py35] @@ -13,11 +13,11 @@ repos: - id: check-yaml - id: debug-statements - repo: https://github.com/PyCQA/flake8 - rev: 3.9.1 + rev: 3.9.2 hooks: - id: flake8 - repo: https://github.com/asottile/pyupgrade - rev: v2.14.0 + rev: v2.15.0 hooks: - id: pyupgrade args: [--py3-plus] From 577e24b5bd52e54c00f71e424812796462f23dc5 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 17 May 2021 17:29:04 +0000 Subject: [PATCH 028/113] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/pre-commit-hooks: v3.4.0 → v4.0.1](https://github.com/pre-commit/pre-commit-hooks/compare/v3.4.0...v4.0.1) - [github.com/asottile/pyupgrade: v2.15.0 → v2.16.0](https://github.com/asottile/pyupgrade/compare/v2.15.0...v2.16.0) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index effa004a..ecd84fb8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -6,7 +6,7 @@ repos: args: [--safe, --quiet, --target-version, py35] language_version: python3.7 - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v3.4.0 + rev: v4.0.1 hooks: - id: trailing-whitespace - id: end-of-file-fixer @@ -17,7 +17,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/asottile/pyupgrade - rev: v2.15.0 + rev: v2.16.0 hooks: - id: pyupgrade args: [--py3-plus] From f75479f7f3b8d5ce4f557d5a0d7932c75f795b15 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 27 May 2021 10:46:23 -0300 Subject: [PATCH 029/113] Document that -s doesn't work with pytest-xdist Fix #296 --- README.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.rst b/README.rst index da5daaf2..cb0a31b8 100644 --- a/README.rst +++ b/README.rst @@ -45,6 +45,9 @@ If you would like to know how pytest-xdist works under the covers, checkout `OVERVIEW `_. +**NOTE**: due to how pytest-xdist is implemented, the ``-s/--capture=no`` option does not work. + + Installation ------------ From 08d8a620407ce50069378528b114775fcdb1f7b0 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 31 May 2021 17:33:03 +0000 Subject: [PATCH 030/113] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/psf/black: 21.5b1 → 21.5b2](https://github.com/psf/black/compare/21.5b1...21.5b2) - [github.com/asottile/pyupgrade: v2.16.0 → v2.19.0](https://github.com/asottile/pyupgrade/compare/v2.16.0...v2.19.0) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ecd84fb8..21418d0f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/psf/black - rev: 21.5b1 + rev: 21.5b2 hooks: - id: black args: [--safe, --quiet, --target-version, py35] @@ -17,7 +17,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/asottile/pyupgrade - rev: v2.16.0 + rev: v2.19.0 hooks: - id: pyupgrade args: [--py3-plus] From 0c84201b40bda2c76927d5463a63394daddcc27c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 31 May 2021 19:13:25 +0000 Subject: [PATCH 031/113] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/xdist/looponfail.py | 2 +- src/xdist/report.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/xdist/looponfail.py b/src/xdist/looponfail.py index 19b9313e..5ce13a02 100644 --- a/src/xdist/looponfail.py +++ b/src/xdist/looponfail.py @@ -61,7 +61,7 @@ def __init__(self, config): def trace(self, *args): if self.config.option.debug: - msg = " ".join([str(x) for x in args]) + msg = " ".join(str(x) for x in args) print("RemoteControl:", msg) def initgateway(self): diff --git a/src/xdist/report.py b/src/xdist/report.py index 8843b40b..02ad30d2 100644 --- a/src/xdist/report.py +++ b/src/xdist/report.py @@ -16,5 +16,5 @@ def report_collection_diff(from_collection, to_collection, from_id, to_id): "The difference is:\n" "{diff}" ).format(from_id=from_id, to_id=to_id, diff="\n".join(diff)) - msg = "\n".join([x.rstrip() for x in error_message.split("\n")]) + msg = "\n".join(x.rstrip() for x in error_message.split("\n")) return msg From 56e631b4bbeedf7ef6da94d3c5eab00c20a07d12 Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Thu, 3 Jun 2021 21:07:01 +0100 Subject: [PATCH 032/113] fix sys.path for local workers Fixes #421 --- src/xdist/plugin.py | 3 +++ src/xdist/remote.py | 4 +++- src/xdist/workermanage.py | 4 +++- testing/test_remote.py | 16 +++++++++++++++- 4 files changed, 24 insertions(+), 3 deletions(-) diff --git a/src/xdist/plugin.py b/src/xdist/plugin.py index 1eba32b8..d5693861 100644 --- a/src/xdist/plugin.py +++ b/src/xdist/plugin.py @@ -1,9 +1,12 @@ import os import uuid +import sys import py import pytest +_sys_path = list(sys.path) # freeze a copy of sys.path at interpreter startup + def pytest_xdist_auto_num_workers(): try: diff --git a/src/xdist/remote.py b/src/xdist/remote.py index 7f95b5cc..d79f0b38 100644 --- a/src/xdist/remote.py +++ b/src/xdist/remote.py @@ -219,12 +219,14 @@ def setup_config(config, basetemp): channel = channel # noqa workerinput, args, option_dict, change_sys_path = channel.receive() - if change_sys_path: + if change_sys_path is None: importpath = os.getcwd() sys.path.insert(0, importpath) os.environ["PYTHONPATH"] = ( importpath + os.pathsep + os.environ.get("PYTHONPATH", "") ) + else: + sys.path = change_sys_path os.environ["PYTEST_XDIST_TESTRUNUID"] = workerinput["testrunuid"] os.environ["PYTEST_XDIST_WORKER"] = workerinput["workerid"] diff --git a/src/xdist/workermanage.py b/src/xdist/workermanage.py index 8fed077e..2c4f1a68 100644 --- a/src/xdist/workermanage.py +++ b/src/xdist/workermanage.py @@ -9,6 +9,7 @@ import execnet import xdist.remote +from xdist.plugin import _sys_path def parse_spec_config(config): @@ -261,7 +262,8 @@ def setup(self): remote_module = self.config.hook.pytest_xdist_getremotemodule() self.channel = self.gateway.remote_exec(remote_module) # change sys.path only for remote workers - change_sys_path = not self.gateway.spec.popen + # restore sys.path from a frozen copy for local workers + change_sys_path = _sys_path if self.gateway.spec.popen else None self.channel.send((self.workerinput, args, option_dict, change_sys_path)) if self.putevent: diff --git a/testing/test_remote.py b/testing/test_remote.py index 2f6e2221..9339fe9f 100644 --- a/testing/test_remote.py +++ b/testing/test_remote.py @@ -234,7 +234,7 @@ def test(): def test_remote_inner_argv(testdir): - """Test/document the behavior due to execnet using `python -c`.""" + """Work around sys.path differences due to execnet using `python -c`.""" testdir.makepyfile( """ import sys @@ -292,3 +292,17 @@ def test(get_config_parser, request): result = testdir.runpytest_subprocess("-n1") assert result.ret == 1 result.stdout.fnmatch_lines(["*usage: *", "*error: my_usage_error"]) + + +def test_remote_sys_path(testdir): + """Test/document the behavior due to execnet using `python -c`.""" + testdir.makepyfile( + """ + import sys + + def test_sys_path(): + assert "" not in sys.path + """ + ) + result = testdir.runpytest("-n1") + assert result.ret == 0 From 02f971d45ff40313d4b31c58396bbd546a120a2b Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Thu, 3 Jun 2021 21:46:21 +0100 Subject: [PATCH 033/113] swap docstring --- testing/test_remote.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/testing/test_remote.py b/testing/test_remote.py index 9339fe9f..31a6a2ae 100644 --- a/testing/test_remote.py +++ b/testing/test_remote.py @@ -234,7 +234,7 @@ def test(): def test_remote_inner_argv(testdir): - """Work around sys.path differences due to execnet using `python -c`.""" + """Test/document the behavior due to execnet using `python -c`.""" testdir.makepyfile( """ import sys @@ -295,7 +295,7 @@ def test(get_config_parser, request): def test_remote_sys_path(testdir): - """Test/document the behavior due to execnet using `python -c`.""" + """Work around sys.path differences due to execnet using `python -c`.""" testdir.makepyfile( """ import sys From 0b14d9289482cdbfe85d73c47b58ecc89ecf1c06 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 8 Jun 2021 01:53:05 +0000 Subject: [PATCH 034/113] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v2.19.0 → v2.19.1](https://github.com/asottile/pyupgrade/compare/v2.19.0...v2.19.1) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 21418d0f..52e2474a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,7 +17,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/asottile/pyupgrade - rev: v2.19.0 + rev: v2.19.1 hooks: - id: pyupgrade args: [--py3-plus] From 958679e9a717fe4cea93aac989f072deb4962f08 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 14 Jun 2021 17:41:19 +0000 Subject: [PATCH 035/113] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/psf/black: 21.5b2 → 21.6b0](https://github.com/psf/black/compare/21.5b2...21.6b0) - [github.com/asottile/pyupgrade: v2.19.1 → v2.19.4](https://github.com/asottile/pyupgrade/compare/v2.19.1...v2.19.4) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 52e2474a..19f2f106 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/psf/black - rev: 21.5b2 + rev: 21.6b0 hooks: - id: black args: [--safe, --quiet, --target-version, py35] @@ -17,7 +17,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/asottile/pyupgrade - rev: v2.19.1 + rev: v2.19.4 hooks: - id: pyupgrade args: [--py3-plus] From b0722675e033b346382475b1ab2e0e32cdb07ae6 Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Wed, 16 Jun 2021 10:33:45 +0100 Subject: [PATCH 036/113] add newsfile --- changelog/421.bugfix.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog/421.bugfix.rst diff --git a/changelog/421.bugfix.rst b/changelog/421.bugfix.rst new file mode 100644 index 00000000..121c69c8 --- /dev/null +++ b/changelog/421.bugfix.rst @@ -0,0 +1 @@ +Copy the parent process sys.path into local workers, to work around execnet's python -c adding the current directory to sys.path. From 26e7d953f41c38c6aca209c63e2f82140def9255 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Wed, 16 Jun 2021 11:46:18 +0200 Subject: [PATCH 037/113] prepare release 2.3.0 --- CHANGELOG.rst | 33 +++++++++++++++++++++++++++++++++ changelog/421.bugfix.rst | 1 - changelog/592.trivial.rst | 1 - changelog/638.bugfix.rst | 1 - changelog/643.trivial.rst | 1 - changelog/646.feature.rst | 3 --- changelog/650.feature.rst | 1 - changelog/654.removal.rst | 1 - 8 files changed, 33 insertions(+), 9 deletions(-) delete mode 100644 changelog/421.bugfix.rst delete mode 100644 changelog/592.trivial.rst delete mode 100644 changelog/638.bugfix.rst delete mode 100644 changelog/643.trivial.rst delete mode 100644 changelog/646.feature.rst delete mode 100644 changelog/650.feature.rst delete mode 100644 changelog/654.removal.rst diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 4e5bb6b0..445f2b43 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,3 +1,36 @@ +pytest-xdist 2.3.0 (2021-06-16) +Deprecations and Removals +------------------------- + +- `#654 `_: Python 3.5 is no longer supported. + + +Features +-------- + +- `#646 `_: Add ``--numprocesses=logical`` flag, which automatically uses the number of logical CPUs available, instead of physical CPUs with ``auto``. + + This is very useful for test suites which are not CPU-bound. + +- `#650 `_: Added new ``pytest_handlecrashitem`` hook to allow handling and rescheduling crashed items. + + +Bug Fixes +--------- + +- `#421 `_: Copy the parent process sys.path into local workers, to work around execnet's python -c adding the current directory to sys.path. + +- `#638 `_: Fix issue caused by changing the branch name of the pytest repository. + + +Trivial Changes +--------------- + +- `#592 `_: Replace master with controller where ever possible. + +- `#643 `_: Use 'main' to refer to pytest default branch in tox env names. + + pytest-xdist 2.2.1 (2021-02-09) =============================== diff --git a/changelog/421.bugfix.rst b/changelog/421.bugfix.rst deleted file mode 100644 index 121c69c8..00000000 --- a/changelog/421.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Copy the parent process sys.path into local workers, to work around execnet's python -c adding the current directory to sys.path. diff --git a/changelog/592.trivial.rst b/changelog/592.trivial.rst deleted file mode 100644 index a240729e..00000000 --- a/changelog/592.trivial.rst +++ /dev/null @@ -1 +0,0 @@ -Replace master with controller where ever possible. diff --git a/changelog/638.bugfix.rst b/changelog/638.bugfix.rst deleted file mode 100644 index 7fb978cb..00000000 --- a/changelog/638.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fix issue caused by changing the branch name of the pytest repository. diff --git a/changelog/643.trivial.rst b/changelog/643.trivial.rst deleted file mode 100644 index 927d2627..00000000 --- a/changelog/643.trivial.rst +++ /dev/null @@ -1 +0,0 @@ -Use 'main' to refer to pytest default branch in tox env names. diff --git a/changelog/646.feature.rst b/changelog/646.feature.rst deleted file mode 100644 index 210c73ee..00000000 --- a/changelog/646.feature.rst +++ /dev/null @@ -1,3 +0,0 @@ -Add ``--numprocesses=logical`` flag, which automatically uses the number of logical CPUs available, instead of physical CPUs with ``auto``. - -This is very useful for test suites which are not CPU-bound. diff --git a/changelog/650.feature.rst b/changelog/650.feature.rst deleted file mode 100644 index ce6dc399..00000000 --- a/changelog/650.feature.rst +++ /dev/null @@ -1 +0,0 @@ -Added new ``pytest_handlecrashitem`` hook to allow handling and rescheduling crashed items. diff --git a/changelog/654.removal.rst b/changelog/654.removal.rst deleted file mode 100644 index b2159e35..00000000 --- a/changelog/654.removal.rst +++ /dev/null @@ -1 +0,0 @@ -Python 3.5 is no longer supported. From fe57b39563aa80be7605302baa090069a172037a Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Wed, 16 Jun 2021 11:49:15 +0200 Subject: [PATCH 038/113] fixup: add release title underline for 2.3.0 --- CHANGELOG.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 445f2b43..29764aa5 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,4 +1,6 @@ pytest-xdist 2.3.0 (2021-06-16) +=============================== + Deprecations and Removals ------------------------- From dba716ba911d1e07ac92b7e7734b0ed31f957628 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 12 Jul 2021 23:28:12 +0000 Subject: [PATCH 039/113] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v2.19.4 → v2.21.0](https://github.com/asottile/pyupgrade/compare/v2.19.4...v2.21.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 19f2f106..9de731b5 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,7 +17,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/asottile/pyupgrade - rev: v2.19.4 + rev: v2.21.0 hooks: - id: pyupgrade args: [--py3-plus] From 1167e1609c67395e92e549802850a7f5ebdb654c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 19 Jul 2021 22:14:44 +0000 Subject: [PATCH 040/113] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/psf/black: 21.6b0 → 21.7b0](https://github.com/psf/black/compare/21.6b0...21.7b0) - [github.com/asottile/pyupgrade: v2.21.0 → v2.21.2](https://github.com/asottile/pyupgrade/compare/v2.21.0...v2.21.2) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9de731b5..eedbd585 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/psf/black - rev: 21.6b0 + rev: 21.7b0 hooks: - id: black args: [--safe, --quiet, --target-version, py35] @@ -17,7 +17,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/asottile/pyupgrade - rev: v2.21.0 + rev: v2.21.2 hooks: - id: pyupgrade args: [--py3-plus] From ccb14f24fc0dfa9b48081c1f974a806db7c79b76 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 26 Jul 2021 18:05:33 +0000 Subject: [PATCH 041/113] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v2.21.2 → v2.23.0](https://github.com/asottile/pyupgrade/compare/v2.21.2...v2.23.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index eedbd585..e3e85515 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,7 +17,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/asottile/pyupgrade - rev: v2.21.2 + rev: v2.23.0 hooks: - id: pyupgrade args: [--py3-plus] From 513e203ee18d4a2201ebad2a1b22c0b385c14778 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 23 Aug 2021 18:59:34 +0000 Subject: [PATCH 042/113] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v2.23.0 → v2.24.0](https://github.com/asottile/pyupgrade/compare/v2.23.0...v2.24.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e3e85515..7f1f927a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,7 +17,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/asottile/pyupgrade - rev: v2.23.0 + rev: v2.24.0 hooks: - id: pyupgrade args: [--py3-plus] From a0ad5cf0417a85ce0372c001b8eb15058952842e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 30 Aug 2021 19:26:09 +0000 Subject: [PATCH 043/113] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/psf/black: 21.7b0 → 21.8b0](https://github.com/psf/black/compare/21.7b0...21.8b0) - [github.com/asottile/pyupgrade: v2.24.0 → v2.25.0](https://github.com/asottile/pyupgrade/compare/v2.24.0...v2.25.0) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7f1f927a..5c32fc86 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/psf/black - rev: 21.7b0 + rev: 21.8b0 hooks: - id: black args: [--safe, --quiet, --target-version, py35] @@ -17,7 +17,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/asottile/pyupgrade - rev: v2.24.0 + rev: v2.25.0 hooks: - id: pyupgrade args: [--py3-plus] From 90fc6169061d7267805714f3187804886c901c5a Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 1 Sep 2021 07:54:13 -0300 Subject: [PATCH 044/113] Fix test_fixture_teardown_failure for pytest master `--debug` now receives an optional file, so the way the test was calling pytest made it seem like it was passing the python file file as the debug file. --- testing/acceptance_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index f7f21bac..3e30e454 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -568,7 +568,7 @@ def test_hello(myarg): pass """ ) - result = testdir.runpytest_subprocess("--debug", p) # , "-n1") + result = testdir.runpytest_subprocess(p, "-n1") result.stdout.fnmatch_lines(["*ValueError*42*", "*1 passed*1 error*"]) assert result.ret From 766e67ce526c88327670e9bcd4804c335df51015 Mon Sep 17 00:00:00 2001 From: David Lamparter Date: Fri, 3 Sep 2021 17:58:01 +0200 Subject: [PATCH 045/113] Use setproctitle if available to show state (#696) Co-authored-by: Bruno Oliveira --- .github/workflows/main.yml | 3 +++ README.rst | 20 ++++++++++++++++++++ changelog/696.feature.rst | 3 +++ setup.py | 6 +++++- src/xdist/remote.py | 20 ++++++++++++++++++++ tox.ini | 9 +++++++++ 6 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 changelog/696.feature.rst diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index c9eb227b..0be930d3 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -17,6 +17,7 @@ jobs: - "py39-pytestlatest" - "py38-pytestmain" - "py38-psutil" + - "py38-setproctitle" os: [ubuntu-latest, windows-latest] include: @@ -32,6 +33,8 @@ jobs: python: "3.8" - tox_env: "py38-psutil" python: "3.8" + - tox_env: "py38-setproctitle" + python: "3.8" steps: - uses: actions/checkout@v1 diff --git a/README.rst b/README.rst index cb0a31b8..5394aad0 100644 --- a/README.rst +++ b/README.rst @@ -317,6 +317,26 @@ Since version 2.0, the following functions are also available in the ``xdist`` m """ +Identifying workers from the system environment +----------------------------------------------- + +*New in version UNRELEASED TBD FIXME* + +If the `setproctitle`_ package is installed, ``pytest-xdist`` will use it to +update the process title (command line) on its workers to show their current +state. The titles used are ``[pytest-xdist running] file.py/node::id`` and +``[pytest-xdist idle]``, visible in standard tools like ``ps`` and ``top`` on +Linux, Mac OS X and BSD systems. For Windows, please follow `setproctitle`_'s +pointer regarding the Process Explorer tool. + +This is intended purely as an UX enhancement, e.g. to track down issues with +long-running or CPU intensive tests. Errors in changing the title are ignored +silently. Please try not to rely on the title format or title changes in +external scripts. + +.. _`setproctitle`: https://pypi.org/project/setproctitle/ + + Uniquely identifying the current test run ----------------------------------------- diff --git a/changelog/696.feature.rst b/changelog/696.feature.rst new file mode 100644 index 00000000..8c78a141 --- /dev/null +++ b/changelog/696.feature.rst @@ -0,0 +1,3 @@ +On Linux, the process title now changes to indicate the current worker state (running/idle). + +Depends on the `setproctitle `__ package, which can be installed with ``pip install pytest-xdist[setproctitle]``. diff --git a/setup.py b/setup.py index 9a75ca52..40b1f905 100644 --- a/setup.py +++ b/setup.py @@ -18,7 +18,11 @@ platforms=["linux", "osx", "win32"], packages=find_packages(where="src"), package_dir={"": "src"}, - extras_require={"testing": ["filelock"], "psutil": ["psutil>=3.0"]}, + extras_require={ + "testing": ["filelock"], + "psutil": ["psutil>=3.0"], + "setproctitle": ["setproctitle"], + }, entry_points={ "pytest11": ["xdist = xdist.plugin", "xdist.looponfail = xdist.looponfail"] }, diff --git a/src/xdist/remote.py b/src/xdist/remote.py index d79f0b38..0194ae02 100644 --- a/src/xdist/remote.py +++ b/src/xdist/remote.py @@ -16,6 +16,21 @@ from _pytest.config import _prepareconfig, Config +try: + from setproctitle import setproctitle +except ImportError: + + def setproctitle(title): + pass + + +def worker_title(title): + try: + setproctitle(title) + except Exception: + # changing the process name is very optional, no errors please + pass + class WorkerInteractor: def __init__(self, config, channel): @@ -85,9 +100,14 @@ def run_one_test(self, torun): else: nextitem = None + worker_title("[pytest-xdist running] %s" % item.nodeid) + start = time.time() self.config.hook.pytest_runtest_protocol(item=item, nextitem=nextitem) duration = time.time() - start + + worker_title("[pytest-xdist idle]") + self.sendevent( "runtest_protocol_complete", item_index=self.item_index, duration=duration ) diff --git a/tox.ini b/tox.ini index 0e4ad977..d5a263c7 100644 --- a/tox.ini +++ b/tox.ini @@ -4,6 +4,7 @@ envlist= py{36,37,38,39}-pytestlatest py38-pytestmain py38-psutil + py38-setproctitle [testenv] extras = testing @@ -21,6 +22,14 @@ deps = pytest commands = pytest {posargs:-k psutil} +[testenv:py38-setproctitle] +extras = + testing + setproctitle +deps = pytest +commands = + pytest {posargs} + [testenv:release] changedir= decription = do a release, required posarg of the version number From 9807064f66d28bbb5aee1bdae0a3f47919c39206 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 13 Sep 2021 19:41:01 +0000 Subject: [PATCH 046/113] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v2.25.0 → v2.26.0](https://github.com/asottile/pyupgrade/compare/v2.25.0...v2.26.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5c32fc86..ea7ca7e7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,7 +17,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/asottile/pyupgrade - rev: v2.25.0 + rev: v2.26.0 hooks: - id: pyupgrade args: [--py3-plus] From ed47f0e01a38af95100f806d0cfc98f96723b721 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Wed, 15 Sep 2021 17:14:59 +0300 Subject: [PATCH 047/113] Add support for Python 3.10 --- .github/workflows/main.yml | 11 +++++++---- setup.py | 1 + tox.ini | 2 +- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 0be930d3..7359cfe4 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -15,6 +15,7 @@ jobs: - "py37-pytestlatest" - "py38-pytestlatest" - "py39-pytestlatest" + - "py310-pytestlatest" - "py38-pytestmain" - "py38-psutil" - "py38-setproctitle" @@ -29,6 +30,8 @@ jobs: python: "3.8" - tox_env: "py39-pytestlatest" python: "3.9" + - tox_env: "py310-pytestlatest" + python: "3.10-dev" - tox_env: "py38-pytestmain" python: "3.8" - tox_env: "py38-psutil" @@ -37,9 +40,9 @@ jobs: python: "3.8" steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v2 - name: Set up Python - uses: actions/setup-python@v1 + uses: actions/setup-python@v2 with: python-version: ${{ matrix.python }} - name: Install tox @@ -59,9 +62,9 @@ jobs: needs: build steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v2 - name: Set up Python - uses: actions/setup-python@v1 + uses: actions/setup-python@v2 with: python-version: "3.7" - name: Install wheel diff --git a/setup.py b/setup.py index 40b1f905..3f1c377f 100644 --- a/setup.py +++ b/setup.py @@ -48,5 +48,6 @@ "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", ], ) diff --git a/tox.ini b/tox.ini index d5a263c7..ed569f49 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,7 @@ [tox] envlist= linting - py{36,37,38,39}-pytestlatest + py{36,37,38,39,310}-pytestlatest py38-pytestmain py38-psutil py38-setproctitle From e0ce1b74186043da5e347d63a47a8d34ee4cd36c Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Wed, 15 Sep 2021 17:28:44 +0300 Subject: [PATCH 048/113] Add news file to add support for Python 3.10 --- changelog/704.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog/704.feature diff --git a/changelog/704.feature b/changelog/704.feature new file mode 100644 index 00000000..d1e7f795 --- /dev/null +++ b/changelog/704.feature @@ -0,0 +1 @@ +Add support for Python 3.10. From 66dc3904b4a0bd1ac491af9eb4e59750f92668f5 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 20 Sep 2021 20:00:03 +0000 Subject: [PATCH 049/113] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/psf/black: 21.8b0 → 21.9b0](https://github.com/psf/black/compare/21.8b0...21.9b0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ea7ca7e7..0d413cd4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/psf/black - rev: 21.8b0 + rev: 21.9b0 hooks: - id: black args: [--safe, --quiet, --target-version, py35] From ecf4d3be31a71365026f0bbb8d67c157f75d695d Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 20 Sep 2021 21:17:31 -0300 Subject: [PATCH 050/113] Release 2.4.0 --- CHANGELOG.rst | 11 +++++++++++ changelog/696.feature.rst | 3 --- changelog/704.feature | 1 - 3 files changed, 11 insertions(+), 4 deletions(-) delete mode 100644 changelog/696.feature.rst delete mode 100644 changelog/704.feature diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 29764aa5..5dbfaa72 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,3 +1,14 @@ +pytest-xdist 2.4.0 (2021-09-20) +Features +-------- + +- `#696 `_: On Linux, the process title now changes to indicate the current worker state (running/idle). + + Depends on the `setproctitle `__ package, which can be installed with ``pip install pytest-xdist[setproctitle]``. + +- `#704 `_: Add support for Python 3.10. + + pytest-xdist 2.3.0 (2021-06-16) =============================== diff --git a/changelog/696.feature.rst b/changelog/696.feature.rst deleted file mode 100644 index 8c78a141..00000000 --- a/changelog/696.feature.rst +++ /dev/null @@ -1,3 +0,0 @@ -On Linux, the process title now changes to indicate the current worker state (running/idle). - -Depends on the `setproctitle `__ package, which can be installed with ``pip install pytest-xdist[setproctitle]``. diff --git a/changelog/704.feature b/changelog/704.feature deleted file mode 100644 index d1e7f795..00000000 --- a/changelog/704.feature +++ /dev/null @@ -1 +0,0 @@ -Add support for Python 3.10. From 4b487ed5e1932992bb2fd92cb0d04df5fac4c843 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 20 Sep 2021 21:19:04 -0300 Subject: [PATCH 051/113] Manually fix changelog title --- CHANGELOG.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 5dbfaa72..e302e069 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,4 +1,6 @@ pytest-xdist 2.4.0 (2021-09-20) +=============================== + Features -------- From 4097c343c20f724be062f0edf303c9616bd762a2 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 21 Sep 2021 09:21:51 -0300 Subject: [PATCH 052/113] Add final note to merge PR in RELEASING.rst --- RELEASING.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/RELEASING.rst b/RELEASING.rst index 078c35b4..5cfd7c01 100644 --- a/RELEASING.rst +++ b/RELEASING.rst @@ -41,3 +41,5 @@ To publish a new release ``X.Y.Z``, the steps are as follows: $ git push git@github.com:pytest-dev/pytest-xdist.git v$VERSION That will build the package and publish it on ``PyPI`` automatically. + +#. Merge the release PR to `master`. From c7c7f9ef5b1ddbde134f11f33cffcb279b5ca160 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 24 Sep 2021 08:17:11 -0300 Subject: [PATCH 053/113] Use modern options configurations for hooks Noticed while reviewing https://github.com/pytest-dev/pytest/pull/9118 --- changelog/708.trivial.rst | 1 + src/xdist/newhooks.py | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) create mode 100644 changelog/708.trivial.rst diff --git a/changelog/708.trivial.rst b/changelog/708.trivial.rst new file mode 100644 index 00000000..38b41769 --- /dev/null +++ b/changelog/708.trivial.rst @@ -0,0 +1 @@ +Use ``@pytest.hookspec`` decorator to declare hook options in ``newhooks.py`` to avoid warnings in ``pytest 7.0``. diff --git a/src/xdist/newhooks.py b/src/xdist/newhooks.py index d2482aff..f9ac6b4d 100644 --- a/src/xdist/newhooks.py +++ b/src/xdist/newhooks.py @@ -30,7 +30,7 @@ def pytest_xdist_rsyncfinish(source, gateways): """called after rsyncing a directory to remote gateways takes place.""" -@pytest.mark.firstresult +@pytest.hookspec(firstresult=True) def pytest_xdist_getremotemodule(): """called when creating remote node""" @@ -51,12 +51,12 @@ def pytest_xdist_node_collection_finished(node, ids): """called by the controller node when a worker node finishes collecting.""" -@pytest.mark.firstresult +@pytest.hookspec(firstresult=True) def pytest_xdist_make_scheduler(config, log): """return a node scheduler implementation""" -@pytest.mark.firstresult +@pytest.hookspec(firstresult=True) def pytest_xdist_auto_num_workers(config): """ Return the number of workers to spawn when ``--numprocesses=auto`` is given in the @@ -66,7 +66,7 @@ def pytest_xdist_auto_num_workers(config): """ -@pytest.mark.firstresult +@pytest.hookspec(firstresult=True) def pytest_handlecrashitem(crashitem, report, sched): """ Handle a crashitem, modifying the report if necessary. From c0ce0c017173d48b5013d20043d968c5627a3198 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 27 Sep 2021 19:44:31 +0000 Subject: [PATCH 054/113] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v2.26.0 → v2.28.0](https://github.com/asottile/pyupgrade/compare/v2.26.0...v2.28.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0d413cd4..d3091c3b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,7 +17,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/asottile/pyupgrade - rev: v2.26.0 + rev: v2.28.0 hooks: - id: pyupgrade args: [--py3-plus] From 3fa5431a53584ee0f5a674b4e71c6e82502e0f14 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 4 Oct 2021 19:50:07 +0000 Subject: [PATCH 055/113] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v2.28.0 → v2.29.0](https://github.com/asottile/pyupgrade/compare/v2.28.0...v2.29.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d3091c3b..57e32f1d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,7 +17,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/asottile/pyupgrade - rev: v2.28.0 + rev: v2.29.0 hooks: - id: pyupgrade args: [--py3-plus] From 4d902461f956422df3a6ba9b1c2de5d499c54dd2 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 11 Oct 2021 19:44:07 +0000 Subject: [PATCH 056/113] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/PyCQA/flake8: 3.9.2 → 4.0.1](https://github.com/PyCQA/flake8/compare/3.9.2...4.0.1) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 57e32f1d..12c67bcc 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -13,7 +13,7 @@ repos: - id: check-yaml - id: debug-statements - repo: https://github.com/PyCQA/flake8 - rev: 3.9.2 + rev: 4.0.1 hooks: - id: flake8 - repo: https://github.com/asottile/pyupgrade From f8a434e3498528591918c25b9672c2e96d16ec6e Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 30 Oct 2021 11:56:31 +0300 Subject: [PATCH 057/113] Move metadata from setup.py to setup.cfg Using https://github.com/asottile/setup-py-upgrade Refs #719. --- setup.cfg | 56 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ setup.py | 57 +++++-------------------------------------------------- 2 files changed, 61 insertions(+), 52 deletions(-) diff --git a/setup.cfg b/setup.cfg index 71037d7b..e442e9c8 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,61 @@ [metadata] +name = pytest-xdist +description = pytest xdist plugin for distributed testing and loop-on-failing modes +long_description = file: README.rst +license = MIT +author = holger krekel and contributors +author_email = pytest-dev@python.org,holger@merlinux.eu +url = https://github.com/pytest-dev/pytest-xdist +platforms = + linux + osx + win32 +classifiers = + Development Status :: 5 - Production/Stable + Framework :: Pytest + Intended Audience :: Developers + License :: OSI Approved :: MIT License + Operating System :: POSIX + Operating System :: Microsoft :: Windows + Operating System :: MacOS :: MacOS X + Topic :: Software Development :: Testing + Topic :: Software Development :: Quality Assurance + Topic :: Utilities + Programming Language :: Python + Programming Language :: Python :: 3 + Programming Language :: Python :: 3 :: Only + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 + Programming Language :: Python :: 3.9 + Programming Language :: Python :: 3.10 license_file = LICENSE +[options] +packages = find: +package_dir = =src +zip_safe = False +python_requires = >=3.6 +install_requires = + execnet>=1.1 + pytest>=6.0.0 + pytest-forked +setup_requires = setuptools_scm + +[options.packages.find] +where = src + +[options.entry_points] +pytest11 = + xdist = xdist.plugin + xdist.looponfail = xdist.looponfail + +[options.extras_require] +testing = + filelock + pytest +psutil = psutil>=3.0 +setproctitle = setproctitle + [flake8] max-line-length = 100 diff --git a/setup.py b/setup.py index 3f1c377f..28aefd7b 100644 --- a/setup.py +++ b/setup.py @@ -1,53 +1,6 @@ -from setuptools import setup, find_packages +from setuptools import setup -install_requires = ["execnet>=1.1", "pytest>=6.0.0", "pytest-forked"] - - -with open("README.rst") as f: - long_description = f.read() - -setup( - name="pytest-xdist", - use_scm_version={"write_to": "src/xdist/_version.py"}, - description="pytest xdist plugin for distributed testing and loop-on-failing modes", - long_description=long_description, - license="MIT", - author="holger krekel and contributors", - author_email="pytest-dev@python.org,holger@merlinux.eu", - url="https://github.com/pytest-dev/pytest-xdist", - platforms=["linux", "osx", "win32"], - packages=find_packages(where="src"), - package_dir={"": "src"}, - extras_require={ - "testing": ["filelock"], - "psutil": ["psutil>=3.0"], - "setproctitle": ["setproctitle"], - }, - entry_points={ - "pytest11": ["xdist = xdist.plugin", "xdist.looponfail = xdist.looponfail"] - }, - zip_safe=False, - python_requires=">=3.6", - install_requires=install_requires, - setup_requires=["setuptools_scm"], - classifiers=[ - "Development Status :: 5 - Production/Stable", - "Framework :: Pytest", - "Intended Audience :: Developers", - "License :: OSI Approved :: MIT License", - "Operating System :: POSIX", - "Operating System :: Microsoft :: Windows", - "Operating System :: MacOS :: MacOS X", - "Topic :: Software Development :: Testing", - "Topic :: Software Development :: Quality Assurance", - "Topic :: Utilities", - "Programming Language :: Python", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - ], -) +if __name__ == "__main__": + setup( + use_scm_version={"write_to": "src/xdist/_version.py"}, + ) From 130dcdef76705d967a1a7d6fb4135babc020cfc4 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 30 Oct 2021 11:57:48 +0300 Subject: [PATCH 058/113] Add build-system to pyproject.toml So doesn't use legacy stuff. Fix #719. --- changelog/719.trivial.rst | 1 + pyproject.toml | 12 ++++++++++++ setup.cfg | 2 +- setup.py | 4 +--- 4 files changed, 15 insertions(+), 4 deletions(-) create mode 100644 changelog/719.trivial.rst diff --git a/changelog/719.trivial.rst b/changelog/719.trivial.rst new file mode 100644 index 00000000..8fdb3b65 --- /dev/null +++ b/changelog/719.trivial.rst @@ -0,0 +1 @@ +Use up-to-date ``setup.cfg``/``pyproject.toml`` packaging setup. diff --git a/pyproject.toml b/pyproject.toml index 94b1be61..72a7f7d7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,15 @@ +[build-system] +requires = [ + # sync with setup.py until we discard non-pep-517/518 + "setuptools>=45.0", + "setuptools-scm[toml]>=6.2.3", + "wheel", +] +build-backend = "setuptools.build_meta" + +[tool.setuptools_scm] +write_to = "src/xdist/_version.py" + [tool.towncrier] package = "xdist" filename = "CHANGELOG.rst" diff --git a/setup.cfg b/setup.cfg index e442e9c8..ad5a0eb9 100644 --- a/setup.cfg +++ b/setup.cfg @@ -40,7 +40,7 @@ install_requires = execnet>=1.1 pytest>=6.0.0 pytest-forked -setup_requires = setuptools_scm +setup_requires = setuptools_scm>=6.0 [options.packages.find] where = src diff --git a/setup.py b/setup.py index 28aefd7b..7f1a1763 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,4 @@ from setuptools import setup if __name__ == "__main__": - setup( - use_scm_version={"write_to": "src/xdist/_version.py"}, - ) + setup() From f51289582eed85ff5d31e4f7a81e756fc9823b0f Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 30 Oct 2021 12:05:39 +0300 Subject: [PATCH 059/113] Bring back tox -e linting It just runs pre-commit, and CI doesn't use i,t but I still find it useful for local development. --- tox.ini | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tox.ini b/tox.ini index ed569f49..9d635039 100644 --- a/tox.ini +++ b/tox.ini @@ -30,6 +30,14 @@ deps = pytest commands = pytest {posargs} +[testenv:linting] +skip_install = True +usedevelop = True +passenv = PRE_COMMIT_HOME +deps = + pre-commit +commands = pre-commit run --all-files --show-diff-on-failure + [testenv:release] changedir= decription = do a release, required posarg of the version number From 999923ba9789c6268bdf2f8d2076c0c9e4e6bd2a Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 30 Oct 2021 12:10:08 +0300 Subject: [PATCH 060/113] Allow black and rst pre-commit hooks to run on any Python 3 version Since py2 is dropped we don't really need to worry about this anymore. --- .pre-commit-config.yaml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 12c67bcc..43a05781 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,7 +4,6 @@ repos: hooks: - id: black args: [--safe, --quiet, --target-version, py35] - language_version: python3.7 - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.0.1 hooks: @@ -29,4 +28,3 @@ repos: files: ^(CHANGELOG.rst|HOWTORELEASE.rst|README.rst|changelog/.*)$ language: python additional_dependencies: [pygments, restructuredtext_lint] - language_version: python3.7 From 5672d858093de3d9f0ff0ee7069161b494e80479 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 30 Oct 2021 20:47:21 +0300 Subject: [PATCH 061/113] Require pytest>=6.2 This makes things much easier in terms of typing. pytest 6.2 has been out for a ~year so if someone updates pytest-xdist they should also update pytest! Fix #720. --- changelog/720.trivial.rst | 1 + setup.cfg | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 changelog/720.trivial.rst diff --git a/changelog/720.trivial.rst b/changelog/720.trivial.rst new file mode 100644 index 00000000..ee671cbb --- /dev/null +++ b/changelog/720.trivial.rst @@ -0,0 +1 @@ +Require pytest>=6.2.0. diff --git a/setup.cfg b/setup.cfg index ad5a0eb9..a80ac39c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -38,7 +38,7 @@ zip_safe = False python_requires = >=3.6 install_requires = execnet>=1.1 - pytest>=6.0.0 + pytest>=6.2.0 pytest-forked setup_requires = setuptools_scm>=6.0 From 9ddb274f23d9f7b28874e9f863882dd778a22ab7 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 30 Oct 2021 11:02:23 +0300 Subject: [PATCH 062/113] Update test suite to modern pytest - Use pytest>=6.2 features (same as install_requires). When we can require >=7, can fix some more typing omissions and version checks. - Replace testdir with pytester - Replace py.test with pytest - Replace tmpdir with tmp_path - Remove (almost) all other uses of py - Add some type annotations (not checked yet) Ref #722. --- setup.cfg | 1 - src/xdist/workermanage.py | 4 +- testing/acceptance_test.py | 489 ++++++++++++++++++----------------- testing/conftest.py | 25 +- testing/test_dsession.py | 91 +++---- testing/test_looponfail.py | 347 ++++++++++++++----------- testing/test_newhooks.py | 26 +- testing/test_plugin.py | 110 ++++---- testing/test_remote.py | 92 +++---- testing/test_workermanage.py | 277 +++++++++++--------- tox.ini | 1 - 11 files changed, 771 insertions(+), 692 deletions(-) diff --git a/setup.cfg b/setup.cfg index a80ac39c..d09d11a5 100644 --- a/setup.cfg +++ b/setup.cfg @@ -53,7 +53,6 @@ pytest11 = [options.extras_require] testing = filelock - pytest psutil = psutil>=3.0 setproctitle = setproctitle diff --git a/src/xdist/workermanage.py b/src/xdist/workermanage.py index 2c4f1a68..7f0bce2f 100644 --- a/src/xdist/workermanage.py +++ b/src/xdist/workermanage.py @@ -423,9 +423,9 @@ def unserialize_warning_message(data): kwargs = {"message": message, "category": category} # access private _WARNING_DETAILS because the attributes vary between Python versions - for attr_name in warnings.WarningMessage._WARNING_DETAILS: + for attr_name in warnings.WarningMessage._WARNING_DETAILS: # type: ignore[attr-defined] if attr_name in ("message", "category"): continue kwargs[attr_name] = data[attr_name] - return warnings.WarningMessage(**kwargs) + return warnings.WarningMessage(**kwargs) # type: ignore[arg-type] diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index 3e30e454..3280aa18 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -1,90 +1,93 @@ import os import re +import shutil +from typing import Dict +from typing import List +from typing import Tuple -import py import pytest import xdist class TestDistribution: - def test_n1_pass(self, testdir): - p1 = testdir.makepyfile( + def test_n1_pass(self, pytester: pytest.Pytester) -> None: + p1 = pytester.makepyfile( """ def test_ok(): pass """ ) - result = testdir.runpytest(p1, "-n1") + result = pytester.runpytest(p1, "-n1") assert result.ret == 0 result.stdout.fnmatch_lines(["*1 passed*"]) - def test_n1_fail(self, testdir): - p1 = testdir.makepyfile( + def test_n1_fail(self, pytester: pytest.Pytester) -> None: + p1 = pytester.makepyfile( """ def test_fail(): assert 0 """ ) - result = testdir.runpytest(p1, "-n1") + result = pytester.runpytest(p1, "-n1") assert result.ret == 1 result.stdout.fnmatch_lines(["*1 failed*"]) - def test_n1_import_error(self, testdir): - p1 = testdir.makepyfile( + def test_n1_import_error(self, pytester: pytest.Pytester) -> None: + p1 = pytester.makepyfile( """ import __import_of_missing_module def test_import(): pass """ ) - result = testdir.runpytest(p1, "-n1") + result = pytester.runpytest(p1, "-n1") assert result.ret == 1 result.stdout.fnmatch_lines( ["E *Error: No module named *__import_of_missing_module*"] ) - def test_n2_import_error(self, testdir): + def test_n2_import_error(self, pytester: pytest.Pytester) -> None: """Check that we don't report the same import error multiple times in distributed mode.""" - p1 = testdir.makepyfile( + p1 = pytester.makepyfile( """ import __import_of_missing_module def test_import(): pass """ ) - result1 = testdir.runpytest(p1, "-n2") - result2 = testdir.runpytest(p1, "-n1") + result1 = pytester.runpytest(p1, "-n2") + result2 = pytester.runpytest(p1, "-n1") assert len(result1.stdout.lines) == len(result2.stdout.lines) - def test_n1_skip(self, testdir): - p1 = testdir.makepyfile( + def test_n1_skip(self, pytester: pytest.Pytester) -> None: + p1 = pytester.makepyfile( """ def test_skip(): import pytest pytest.skip("myreason") """ ) - result = testdir.runpytest(p1, "-n1") + result = pytester.runpytest(p1, "-n1") assert result.ret == 0 result.stdout.fnmatch_lines(["*1 skipped*"]) - def test_manytests_to_one_import_error(self, testdir): - p1 = testdir.makepyfile( + def test_manytests_to_one_import_error(self, pytester: pytest.Pytester) -> None: + p1 = pytester.makepyfile( """ import __import_of_missing_module def test_import(): pass """ ) - result = testdir.runpytest(p1, "--tx=popen", "--tx=popen") + result = pytester.runpytest(p1, "--tx=popen", "--tx=popen") assert result.ret in (1, 2) result.stdout.fnmatch_lines( ["E *Error: No module named *__import_of_missing_module*"] ) - def test_manytests_to_one_popen(self, testdir): - p1 = testdir.makepyfile( + def test_manytests_to_one_popen(self, pytester: pytest.Pytester) -> None: + p1 = pytester.makepyfile( """ import pytest def test_fail0(): @@ -97,12 +100,12 @@ def test_skip(): pytest.skip("hello") """ ) - result = testdir.runpytest(p1, "-v", "-d", "--tx=popen", "--tx=popen") + result = pytester.runpytest(p1, "-v", "-d", "--tx=popen", "--tx=popen") result.stdout.fnmatch_lines(["*1*Python*", "*2 failed, 1 passed, 1 skipped*"]) assert result.ret == 1 - def test_n1_fail_minus_x(self, testdir): - p1 = testdir.makepyfile( + def test_n1_fail_minus_x(self, pytester: pytest.Pytester) -> None: + p1 = pytester.makepyfile( """ def test_fail1(): assert 0 @@ -110,25 +113,25 @@ def test_fail2(): assert 0 """ ) - result = testdir.runpytest(p1, "-x", "-v", "-n1") + result = pytester.runpytest(p1, "-x", "-v", "-n1") assert result.ret == 2 result.stdout.fnmatch_lines(["*Interrupted: stopping*1*", "*1 failed*"]) - def test_basetemp_in_subprocesses(self, testdir): - p1 = testdir.makepyfile( + def test_basetemp_in_subprocesses(self, pytester: pytest.Pytester) -> None: + p1 = pytester.makepyfile( """ - def test_send(tmpdir): - import py - assert tmpdir.relto(py.path.local(%r)), tmpdir + def test_send(tmp_path): + from pathlib import Path + assert tmp_path.relative_to(Path(%r)), tmp_path """ - % str(testdir.tmpdir) + % str(pytester.path) ) - result = testdir.runpytest_subprocess(p1, "-n1") + result = pytester.runpytest_subprocess(p1, "-n1") assert result.ret == 0 result.stdout.fnmatch_lines(["*1 passed*"]) - def test_dist_ini_specified(self, testdir): - p1 = testdir.makepyfile( + def test_dist_ini_specified(self, pytester: pytest.Pytester) -> None: + p1 = pytester.makepyfile( """ import pytest def test_fail0(): @@ -141,22 +144,22 @@ def test_skip(): pytest.skip("hello") """ ) - testdir.makeini( + pytester.makeini( """ [pytest] addopts = --tx=3*popen """ ) - result = testdir.runpytest(p1, "-d", "-v") + result = pytester.runpytest(p1, "-d", "-v") result.stdout.fnmatch_lines(["*2*Python*", "*2 failed, 1 passed, 1 skipped*"]) assert result.ret == 1 @pytest.mark.xfail("sys.platform.startswith('java')", run=False) - def test_dist_tests_with_crash(self, testdir): + def test_dist_tests_with_crash(self, pytester: pytest.Pytester) -> None: if not hasattr(os, "kill"): pytest.skip("no os.kill") - p1 = testdir.makepyfile( + p1 = pytester.makepyfile( """ import pytest def test_fail0(): @@ -174,7 +177,7 @@ def test_crash(): os.kill(os.getpid(), 15) """ ) - result = testdir.runpytest(p1, "-v", "-d", "-n1") + result = pytester.runpytest(p1, "-v", "-d", "-n1") result.stdout.fnmatch_lines( [ "*Python*", @@ -185,10 +188,12 @@ def test_crash(): ) assert result.ret == 1 - def test_distribution_rsyncdirs_example(self, testdir, monkeypatch): + def test_distribution_rsyncdirs_example( + self, pytester: pytest.Pytester, monkeypatch + ) -> None: # use a custom plugin that has a custom command-line option to ensure # this is propagated to workers (see #491) - testdir.makepyfile( + pytester.makepyfile( **{ "myplugin/src/foobarplugin.py": """ from __future__ import print_function @@ -207,18 +212,19 @@ def pytest_load_initial_conftests(early_config): """ } ) - assert (testdir.tmpdir / "myplugin/src/foobarplugin.py").check(file=1) + assert (pytester.path / "myplugin/src/foobarplugin.py").is_file() monkeypatch.setenv( - "PYTHONPATH", str(testdir.tmpdir / "myplugin/src"), prepend=os.pathsep + "PYTHONPATH", str(pytester.path / "myplugin/src"), prepend=os.pathsep ) - source = testdir.mkdir("source") - dest = testdir.mkdir("dest") - subdir = source.mkdir("example_pkg") - subdir.ensure("__init__.py") - p = subdir.join("test_one.py") - p.write("def test_5():\n assert not __file__.startswith(%r)" % str(p)) - result = testdir.runpytest_subprocess( + source = pytester.mkdir("source") + dest = pytester.mkdir("dest") + subdir = source / "example_pkg" + subdir.mkdir() + subdir.joinpath("__init__.py").touch() + p = subdir / "test_one.py" + p.write_text("def test_5():\n assert not __file__.startswith(%r)" % str(p)) + result = pytester.runpytest_subprocess( "-v", "-d", "-s", @@ -239,10 +245,10 @@ def pytest_load_initial_conftests(early_config): ] ) result.stderr.fnmatch_lines(["--foobar=123 active! *"]) - assert dest.join(subdir.basename).check(dir=1) + assert dest.joinpath(subdir.name).is_dir() - def test_data_exchange(self, testdir): - testdir.makeconftest( + def test_data_exchange(self, pytester: pytest.Pytester) -> None: + pytester.makeconftest( """ # This hook only called on the controlling process. def pytest_configure_node(node): @@ -268,22 +274,22 @@ def pytest_terminal_summary(terminalreporter): 'calculated result is %s' % calc_result) """ ) - p1 = testdir.makepyfile("def test_func(): pass") - result = testdir.runpytest("-v", p1, "-d", "--tx=popen") + p1 = pytester.makepyfile("def test_func(): pass") + result = pytester.runpytest("-v", p1, "-d", "--tx=popen") result.stdout.fnmatch_lines( ["*0*Python*", "*calculated result is 49*", "*1 passed*"] ) assert result.ret == 0 - def test_keyboardinterrupt_hooks_issue79(self, testdir): - testdir.makepyfile( + def test_keyboardinterrupt_hooks_issue79(self, pytester: pytest.Pytester) -> None: + pytester.makepyfile( __init__="", test_one=""" def test_hello(): raise KeyboardInterrupt() """, ) - testdir.makeconftest( + pytester.makeconftest( """ def pytest_sessionfinish(session): # on the worker @@ -296,22 +302,22 @@ def pytest_testnodedown(node, error): """ ) args = ["-n1", "--debug"] - result = testdir.runpytest_subprocess(*args) + result = pytester.runpytest_subprocess(*args) s = result.stdout.str() assert result.ret == 2 assert "s2call" in s assert "Interrupted" in s - def test_keyboard_interrupt_dist(self, testdir): + def test_keyboard_interrupt_dist(self, pytester: pytest.Pytester) -> None: # xxx could be refined to check for return code - testdir.makepyfile( + pytester.makepyfile( """ def test_sleep(): import time time.sleep(10) """ ) - child = testdir.spawn_pytest("-n1 -v", expect_timeout=30.0) + child = pytester.spawn_pytest("-n1 -v", expect_timeout=30.0) child.expect(".*test_sleep.*") child.kill(2) # keyboard interrupt child.expect(".*KeyboardInterrupt.*") @@ -319,42 +325,42 @@ def test_sleep(): child.close() # assert ret == 2 - def test_dist_with_collectonly(self, testdir): - p1 = testdir.makepyfile( + def test_dist_with_collectonly(self, pytester: pytest.Pytester) -> None: + p1 = pytester.makepyfile( """ def test_ok(): pass """ ) - result = testdir.runpytest(p1, "-n1", "--collect-only") + result = pytester.runpytest(p1, "-n1", "--collect-only") assert result.ret == 0 result.stdout.fnmatch_lines(["*collected 1 item*"]) class TestDistEach: - def test_simple(self, testdir): - testdir.makepyfile( + def test_simple(self, pytester: pytest.Pytester) -> None: + pytester.makepyfile( """ def test_hello(): pass """ ) - result = testdir.runpytest_subprocess("--debug", "--dist=each", "--tx=2*popen") + result = pytester.runpytest_subprocess("--debug", "--dist=each", "--tx=2*popen") assert not result.ret result.stdout.fnmatch_lines(["*2 pass*"]) @pytest.mark.xfail( - run=False, reason="other python versions might not have py.test installed" + run=False, reason="other python versions might not have pytest installed" ) - def test_simple_diffoutput(self, testdir): + def test_simple_diffoutput(self, pytester: pytest.Pytester) -> None: interpreters = [] for name in ("python2.5", "python2.6"): - interp = py.path.local.sysfind(name) + interp = shutil.which(name) if interp is None: pytest.skip("%s not found" % name) interpreters.append(interp) - testdir.makepyfile( + pytester.makepyfile( __init__="", test_one=""" import sys @@ -366,7 +372,7 @@ def test_hello(): args = ["--dist=each", "-v"] args += ["--tx", "popen//python=%s" % interpreters[0]] args += ["--tx", "popen//python=%s" % interpreters[1]] - result = testdir.runpytest(*args) + result = pytester.runpytest(*args) s = result.stdout.str() assert "2...5" in s assert "2...6" in s @@ -374,8 +380,8 @@ def test_hello(): class TestTerminalReporting: @pytest.mark.parametrize("verbosity", ["", "-q", "-v"]) - def test_output_verbosity(self, testdir, verbosity): - testdir.makepyfile( + def test_output_verbosity(self, pytester, verbosity: str) -> None: + pytester.makepyfile( """ def test_ok(): pass @@ -384,7 +390,7 @@ def test_ok(): args = ["-n1"] if verbosity: args.append(verbosity) - result = testdir.runpytest(*args) + result = pytester.runpytest(*args) out = result.stdout.str() if verbosity == "-v": assert "scheduling tests" in out @@ -397,8 +403,8 @@ def test_ok(): assert "scheduling tests" not in out assert "gw" in out - def test_pass_skip_fail(self, testdir): - testdir.makepyfile( + def test_pass_skip_fail(self, pytester: pytest.Pytester) -> None: + pytester.makepyfile( """ import pytest def test_ok(): @@ -409,7 +415,7 @@ def test_func(): assert 0 """ ) - result = testdir.runpytest("-n1", "-v") + result = pytester.runpytest("-n1", "-v") result.stdout.fnmatch_lines_random( [ "*PASS*test_pass_skip_fail.py*test_ok*", @@ -421,14 +427,14 @@ def test_func(): ["*def test_func():", "> assert 0", "E assert 0"] ) - def test_fail_platinfo(self, testdir): - testdir.makepyfile( + def test_fail_platinfo(self, pytester: pytest.Pytester) -> None: + pytester.makepyfile( """ def test_func(): assert 0 """ ) - result = testdir.runpytest("-n1", "-v") + result = pytester.runpytest("-n1", "-v") result.stdout.fnmatch_lines( [ "*FAIL*test_fail_platinfo.py*test_func*", @@ -439,31 +445,31 @@ def test_func(): ] ) - def test_logfinish_hook(self, testdir): + def test_logfinish_hook(self, pytester: pytest.Pytester) -> None: """Ensure the pytest_runtest_logfinish hook is being properly handled""" from _pytest import hookspec if not hasattr(hookspec, "pytest_runtest_logfinish"): pytest.skip("test requires pytest_runtest_logfinish hook in pytest (3.4+)") - testdir.makeconftest( + pytester.makeconftest( """ def pytest_runtest_logfinish(): print('pytest_runtest_logfinish hook called') """ ) - testdir.makepyfile( + pytester.makepyfile( """ def test_func(): pass """ ) - result = testdir.runpytest("-n1", "-s") + result = pytester.runpytest("-n1", "-s") result.stdout.fnmatch_lines(["*pytest_runtest_logfinish hook called*"]) -def test_teardownfails_one_function(testdir): - p = testdir.makepyfile( +def test_teardownfails_one_function(pytester: pytest.Pytester) -> None: + p = pytester.makepyfile( """ def test_func(): pass @@ -471,15 +477,15 @@ def teardown_function(function): assert 0 """ ) - result = testdir.runpytest(p, "-n1", "--tx=popen") + result = pytester.runpytest(p, "-n1", "--tx=popen") result.stdout.fnmatch_lines( ["*def teardown_function(function):*", "*1 passed*1 error*"] ) @pytest.mark.xfail -def test_terminate_on_hangingnode(testdir): - p = testdir.makeconftest( +def test_terminate_on_hangingnode(pytester: pytest.Pytester) -> None: + p = pytester.makeconftest( """ def pytest_sessionfinish(session): if session.nodeid == "my": # running on worker @@ -487,14 +493,14 @@ def pytest_sessionfinish(session): time.sleep(3) """ ) - result = testdir.runpytest(p, "--dist=each", "--tx=popen//id=my") + result = pytester.runpytest(p, "--dist=each", "--tx=popen//id=my") assert result.duration < 2.0 result.stdout.fnmatch_lines(["*killed*my*"]) @pytest.mark.xfail(reason="works if run outside test suite", run=False) -def test_session_hooks(testdir): - testdir.makeconftest( +def test_session_hooks(pytester: pytest.Pytester) -> None: + pytester.makeconftest( """ import sys def pytest_sessionstart(session): @@ -511,28 +517,28 @@ def pytest_sessionfinish(session): raise ValueError(42) """ ) - p = testdir.makepyfile( + p = pytester.makepyfile( """ import sys def test_hello(): assert hasattr(sys, 'pytestsessionhooks') """ ) - result = testdir.runpytest(p, "--dist=each", "--tx=popen") + result = pytester.runpytest(p, "--dist=each", "--tx=popen") result.stdout.fnmatch_lines(["*ValueError*", "*1 passed*"]) assert not result.ret d = result.parseoutcomes() assert d["passed"] == 1 - assert testdir.tmpdir.join("worker").check() - assert testdir.tmpdir.join("controller").check() + assert pytester.path.joinpath("worker").exists() + assert pytester.path.joinpath("controller").exists() -def test_session_testscollected(testdir): +def test_session_testscollected(pytester: pytest.Pytester) -> None: """ Make sure controller node is updating the session object with the number of tests collected from the workers. """ - testdir.makepyfile( + pytester.makepyfile( test_foo=""" import pytest @pytest.mark.parametrize('i', range(3)) @@ -540,7 +546,7 @@ def test_ok(i): pass """ ) - testdir.makeconftest( + pytester.makeconftest( """ def pytest_sessionfinish(session): collected = getattr(session, 'testscollected', None) @@ -548,15 +554,15 @@ def pytest_sessionfinish(session): f.write('collected = %s' % collected) """ ) - result = testdir.inline_run("-n1") + result = pytester.inline_run("-n1") result.assertoutcome(passed=3) - collected_file = testdir.tmpdir.join("testscollected") - assert collected_file.isfile() - assert collected_file.read() == "collected = 3" + collected_file = pytester.path / "testscollected" + assert collected_file.is_file() + assert collected_file.read_text() == "collected = 3" -def test_fixture_teardown_failure(testdir): - p = testdir.makepyfile( +def test_fixture_teardown_failure(pytester: pytest.Pytester) -> None: + p = pytester.makepyfile( """ import pytest @pytest.fixture(scope="module") @@ -568,14 +574,16 @@ def test_hello(myarg): pass """ ) - result = testdir.runpytest_subprocess(p, "-n1") + result = pytester.runpytest_subprocess(p, "-n1") result.stdout.fnmatch_lines(["*ValueError*42*", "*1 passed*1 error*"]) assert result.ret -def test_config_initialization(testdir, monkeypatch, pytestconfig): +def test_config_initialization( + pytester: pytest.Pytester, monkeypatch: pytest.MonkeyPatch, pytestconfig +) -> None: """Ensure workers and controller are initialized consistently. Integration test for #445""" - testdir.makepyfile( + pytester.makepyfile( **{ "dir_a/test_foo.py": """ def test_1(request): @@ -583,7 +591,7 @@ def test_1(request): """ } ) - testdir.makefile( + pytester.makefile( ".ini", myconfig=""" [pytest] @@ -591,17 +599,17 @@ def test_1(request): """, ) monkeypatch.setenv("PYTEST_ADDOPTS", "-v") - result = testdir.runpytest("-n2", "-c", "myconfig.ini", "-v") + result = pytester.runpytest("-n2", "-c", "myconfig.ini", "-v") result.stdout.fnmatch_lines(["dir_a/test_foo.py::test_1*", "*= 1 passed in *"]) assert result.ret == 0 @pytest.mark.parametrize("when", ["setup", "call", "teardown"]) -def test_crashing_item(testdir, when): +def test_crashing_item(pytester, when) -> None: """Ensure crashing item is correctly reported during all testing stages""" code = dict(setup="", call="", teardown="") code[when] = "py.process.kill(os.getpid())" - p = testdir.makepyfile( + p = pytester.makepyfile( """ import os import py @@ -624,19 +632,19 @@ def test_ok(): ) ) passes = 2 if when == "teardown" else 1 - result = testdir.runpytest("-n2", p) + result = pytester.runpytest("-n2", p) result.stdout.fnmatch_lines( ["*crashed*test_crash*", "*1 failed*%d passed*" % passes] ) -def test_multiple_log_reports(testdir): +def test_multiple_log_reports(pytester: pytest.Pytester) -> None: """ Ensure that pytest-xdist supports plugins that emit multiple logreports (#206). Inspired by pytest-rerunfailures. """ - testdir.makeconftest( + pytester.makeconftest( """ from _pytest.runner import runtestprotocol def pytest_runtest_protocol(item, nextitem): @@ -648,31 +656,31 @@ def pytest_runtest_protocol(item, nextitem): return True """ ) - testdir.makepyfile( + pytester.makepyfile( """ def test(): pass """ ) - result = testdir.runpytest("-n1") + result = pytester.runpytest("-n1") result.stdout.fnmatch_lines(["*2 passed*"]) -def test_skipping(testdir): - p = testdir.makepyfile( +def test_skipping(pytester: pytest.Pytester) -> None: + p = pytester.makepyfile( """ import pytest def test_crash(): pytest.skip("hello") """ ) - result = testdir.runpytest("-n1", "-rs", p) + result = pytester.runpytest("-n1", "-rs", p) assert result.ret == 0 result.stdout.fnmatch_lines(["*hello*", "*1 skipped*"]) -def test_fixture_scope_caching_issue503(testdir): - p1 = testdir.makepyfile( +def test_fixture_scope_caching_issue503(pytester: pytest.Pytester) -> None: + p1 = pytester.makepyfile( """ import pytest @@ -690,17 +698,17 @@ def test_b(fix): pass """ ) - result = testdir.runpytest(p1, "-v", "-n1") + result = pytester.runpytest(p1, "-v", "-n1") assert result.ret == 0 result.stdout.fnmatch_lines(["*2 passed*"]) -def test_issue_594_random_parametrize(testdir): +def test_issue_594_random_parametrize(pytester: pytest.Pytester) -> None: """ Make sure that tests that are randomly parametrized display an appropriate error message, instead of silently skipping the entire test run. """ - p1 = testdir.makepyfile( + p1 = pytester.makepyfile( """ import pytest import random @@ -712,42 +720,42 @@ def test_foo(x): assert 1 """ ) - result = testdir.runpytest(p1, "-v", "-n4") + result = pytester.runpytest(p1, "-v", "-n4") assert result.ret == 1 result.stdout.fnmatch_lines(["Different tests were collected between gw* and gw*"]) -def test_tmpdir_disabled(testdir): +def test_tmpdir_disabled(pytester: pytest.Pytester) -> None: """Test xdist doesn't break if internal tmpdir plugin is disabled (#22).""" - p1 = testdir.makepyfile( + p1 = pytester.makepyfile( """ def test_ok(): pass """ ) - result = testdir.runpytest(p1, "-n1", "-p", "no:tmpdir") + result = pytester.runpytest(p1, "-n1", "-p", "no:tmpdir") assert result.ret == 0 result.stdout.fnmatch_lines("*1 passed*") @pytest.mark.parametrize("plugin", ["xdist.looponfail", "xdist.boxed"]) -def test_sub_plugins_disabled(testdir, plugin): +def test_sub_plugins_disabled(pytester, plugin) -> None: """Test that xdist doesn't break if we disable any of its sub-plugins. (#32)""" - p1 = testdir.makepyfile( + p1 = pytester.makepyfile( """ def test_ok(): pass """ ) - result = testdir.runpytest(p1, "-n1", "-p", "no:%s" % plugin) + result = pytester.runpytest(p1, "-n1", "-p", "no:%s" % plugin) assert result.ret == 0 result.stdout.fnmatch_lines("*1 passed*") class TestWarnings: @pytest.mark.parametrize("n", ["-n0", "-n1"]) - def test_warnings(self, testdir, n): - testdir.makepyfile( + def test_warnings(self, pytester, n) -> None: + pytester.makepyfile( """ import warnings, py, pytest @@ -756,19 +764,16 @@ def test_func(request): warnings.warn(UserWarning('this is a warning')) """ ) - result = testdir.runpytest(n) + result = pytester.runpytest(n) result.stdout.fnmatch_lines(["*this is a warning*", "*1 passed, 1 warning*"]) - def test_warning_captured_deprecated_in_pytest_6(self, testdir): + def test_warning_captured_deprecated_in_pytest_6( + self, pytester: pytest.Pytester + ) -> None: """ Do not trigger the deprecated pytest_warning_captured hook in pytest 6+ (#562) """ - import _pytest.hookspec - - if not hasattr(_pytest.hookspec, "pytest_warning_recorded"): - pytest.skip("test requires pytest 6.0+") - - testdir.makeconftest( + pytester.makeconftest( """ def pytest_warning_captured(warning_message): if warning_message == "my custom worker warning": @@ -778,23 +783,23 @@ def pytest_warning_captured(warning_message): ).format(warning_message) """ ) - testdir.makepyfile( + pytester.makepyfile( """ import warnings def test(): warnings.warn("my custom worker warning") """ ) - result = testdir.runpytest("-n1") + result = pytester.runpytest("-n1") result.stdout.fnmatch_lines(["*1 passed*"]) result.stdout.no_fnmatch_line("*this hook should not be called in this version") @pytest.mark.parametrize("n", ["-n0", "-n1"]) - def test_custom_subclass(self, testdir, n): + def test_custom_subclass(self, pytester, n) -> None: """Check that warning subclasses that don't honor the args attribute don't break pytest-xdist (#344) """ - testdir.makepyfile( + pytester.makepyfile( """ import warnings, py, pytest @@ -809,33 +814,34 @@ def test_func(request): warnings.warn(MyWarning("foo", 1)) """ ) - testdir.syspathinsert() - result = testdir.runpytest(n) + pytester.syspathinsert() + result = pytester.runpytest(n) result.stdout.fnmatch_lines(["*MyWarning*", "*1 passed, 1 warning*"]) @pytest.mark.parametrize("n", ["-n0", "-n1"]) - def test_unserializable_arguments(self, testdir, n): + def test_unserializable_arguments(self, pytester, n) -> None: """Check that warnings with unserializable arguments are handled correctly (#349).""" - testdir.makepyfile( + pytester.makepyfile( """ import warnings, pytest - def test_func(tmpdir): - fn = (tmpdir / 'foo.txt').ensure(file=1) + def test_func(tmp_path): + fn = tmp_path / 'foo.txt' + fn.touch() with fn.open('r') as f: warnings.warn(UserWarning("foo", f)) """ ) - testdir.syspathinsert() - result = testdir.runpytest(n) + pytester.syspathinsert() + result = pytester.runpytest(n) result.stdout.fnmatch_lines(["*UserWarning*foo.txt*", "*1 passed, 1 warning*"]) @pytest.mark.parametrize("n", ["-n0", "-n1"]) - def test_unserializable_warning_details(self, testdir, n): + def test_unserializable_warning_details(self, pytester, n) -> None: """Check that warnings with unserializable _WARNING_DETAILS are handled correctly (#379). """ - testdir.makepyfile( + pytester.makepyfile( """ import warnings, pytest import socket @@ -849,28 +855,28 @@ def abuse_socket(): # _WARNING_DETAIL. We need to test that it is not serialized # (it can't be, so the test will fail if we try to). @pytest.mark.filterwarnings('always') - def test_func(tmpdir): + def test_func(tmp_path): abuse_socket() gc.collect() """ ) - testdir.syspathinsert() - result = testdir.runpytest(n) + pytester.syspathinsert() + result = pytester.runpytest(n) result.stdout.fnmatch_lines( ["*ResourceWarning*unclosed*", "*1 passed, 1 warning*"] ) class TestNodeFailure: - def test_load_single(self, testdir): - f = testdir.makepyfile( + def test_load_single(self, pytester: pytest.Pytester) -> None: + f = pytester.makepyfile( """ import os def test_a(): os._exit(1) def test_b(): pass """ ) - res = testdir.runpytest(f, "-n1") + res = pytester.runpytest(f, "-n1") res.stdout.fnmatch_lines( [ "replacing crashed worker gw*", @@ -879,8 +885,8 @@ def test_b(): pass ] ) - def test_load_multiple(self, testdir): - f = testdir.makepyfile( + def test_load_multiple(self, pytester: pytest.Pytester) -> None: + f = pytester.makepyfile( """ import os def test_a(): pass @@ -889,7 +895,7 @@ def test_c(): pass def test_d(): pass """ ) - res = testdir.runpytest(f, "-n2") + res = pytester.runpytest(f, "-n2") res.stdout.fnmatch_lines( [ "replacing crashed worker gw*", @@ -898,15 +904,15 @@ def test_d(): pass ] ) - def test_each_single(self, testdir): - f = testdir.makepyfile( + def test_each_single(self, pytester: pytest.Pytester) -> None: + f = pytester.makepyfile( """ import os def test_a(): os._exit(1) def test_b(): pass """ ) - res = testdir.runpytest(f, "--dist=each", "--tx=popen") + res = pytester.runpytest(f, "--dist=each", "--tx=popen") res.stdout.fnmatch_lines( [ "replacing crashed worker gw*", @@ -916,15 +922,15 @@ def test_b(): pass ) @pytest.mark.xfail(reason="#20: xdist race condition on node restart") - def test_each_multiple(self, testdir): - f = testdir.makepyfile( + def test_each_multiple(self, pytester: pytest.Pytester) -> None: + f = pytester.makepyfile( """ import os def test_a(): os._exit(1) def test_b(): pass """ ) - res = testdir.runpytest(f, "--dist=each", "--tx=2*popen") + res = pytester.runpytest(f, "--dist=each", "--tx=2*popen") res.stdout.fnmatch_lines( [ "*Replacing crashed worker*", @@ -933,8 +939,8 @@ def test_b(): pass ] ) - def test_max_worker_restart(self, testdir): - f = testdir.makepyfile( + def test_max_worker_restart(self, pytester: pytest.Pytester) -> None: + f = pytester.makepyfile( """ import os def test_a(): pass @@ -943,7 +949,7 @@ def test_c(): os._exit(1) def test_d(): pass """ ) - res = testdir.runpytest(f, "-n4", "--max-worker-restart=1") + res = pytester.runpytest(f, "-n4", "--max-worker-restart=1") res.stdout.fnmatch_lines( [ "replacing crashed worker*", @@ -954,15 +960,15 @@ def test_d(): pass ] ) - def test_max_worker_restart_tests_queued(self, testdir): - f = testdir.makepyfile( + def test_max_worker_restart_tests_queued(self, pytester: pytest.Pytester) -> None: + f = pytester.makepyfile( """ import os, pytest @pytest.mark.parametrize('i', range(10)) def test(i): os._exit(1) """ ) - res = testdir.runpytest(f, "-n2", "--max-worker-restart=3") + res = pytester.runpytest(f, "-n2", "--max-worker-restart=3") res.stdout.fnmatch_lines( [ "replacing crashed worker*", @@ -975,14 +981,14 @@ def test(i): os._exit(1) ) assert "INTERNALERROR" not in res.stdout.str() - def test_max_worker_restart_die(self, testdir): - f = testdir.makepyfile( + def test_max_worker_restart_die(self, pytester: pytest.Pytester) -> None: + f = pytester.makepyfile( """ import os os._exit(1) """ ) - res = testdir.runpytest(f, "-n4", "--max-worker-restart=0") + res = pytester.runpytest(f, "-n4", "--max-worker-restart=0") res.stdout.fnmatch_lines( [ "* xdist: worker gw* crashed and worker restarting disabled *", @@ -990,8 +996,8 @@ def test_max_worker_restart_die(self, testdir): ] ) - def test_disable_restart(self, testdir): - f = testdir.makepyfile( + def test_disable_restart(self, pytester: pytest.Pytester) -> None: + f = pytester.makepyfile( """ import os def test_a(): pass @@ -999,7 +1005,7 @@ def test_b(): os._exit(1) def test_c(): pass """ ) - res = testdir.runpytest(f, "-n4", "--max-worker-restart=0") + res = pytester.runpytest(f, "-n4", "--max-worker-restart=0") res.stdout.fnmatch_lines( [ "worker gw* crashed and worker restarting disabled", @@ -1011,10 +1017,10 @@ def test_c(): pass @pytest.mark.parametrize("n", [0, 2]) -def test_worker_id_fixture(testdir, n): +def test_worker_id_fixture(pytester, n) -> None: import glob - f = testdir.makepyfile( + f = pytester.makepyfile( """ import pytest @pytest.mark.parametrize("run_num", range(2)) @@ -1023,10 +1029,10 @@ def test_worker_id1(worker_id, run_num): f.write(worker_id) """ ) - result = testdir.runpytest(f, "-n%d" % n) + result = pytester.runpytest(f, "-n%d" % n) result.stdout.fnmatch_lines("* 2 passed in *") worker_ids = set() - for fname in glob.glob(str(testdir.tmpdir.join("*.txt"))): + for fname in glob.glob(str(pytester.path / "*.txt")): with open(fname) as f: worker_ids.add(f.read().strip()) if n == 0: @@ -1036,10 +1042,10 @@ def test_worker_id1(worker_id, run_num): @pytest.mark.parametrize("n", [0, 2]) -def test_testrun_uid_fixture(testdir, n): +def test_testrun_uid_fixture(pytester, n) -> None: import glob - f = testdir.makepyfile( + f = pytester.makepyfile( """ import pytest @pytest.mark.parametrize("run_num", range(2)) @@ -1048,10 +1054,10 @@ def test_testrun_uid1(testrun_uid, run_num): f.write(testrun_uid) """ ) - result = testdir.runpytest(f, "-n%d" % n) + result = pytester.runpytest(f, "-n%d" % n) result.stdout.fnmatch_lines("* 2 passed in *") testrun_uids = set() - for fname in glob.glob(str(testdir.tmpdir.join("*.txt"))): + for fname in glob.glob(str(pytester.path / "*.txt")): with open(fname) as f: testrun_uids.add(f.read().strip()) assert len(testrun_uids) == 1 @@ -1059,21 +1065,21 @@ def test_testrun_uid1(testrun_uid, run_num): @pytest.mark.parametrize("tb", ["auto", "long", "short", "no", "line", "native"]) -def test_error_report_styles(testdir, tb): - testdir.makepyfile( +def test_error_report_styles(pytester, tb) -> None: + pytester.makepyfile( """ import pytest def test_error_report_styles(): raise RuntimeError('some failure happened') """ ) - result = testdir.runpytest("-n1", "--tb=%s" % tb) + result = pytester.runpytest("-n1", "--tb=%s" % tb) if tb != "no": result.stdout.fnmatch_lines("*some failure happened*") result.assert_outcomes(failed=1) -def test_color_yes_collection_on_non_atty(testdir, request): +def test_color_yes_collection_on_non_atty(pytester, request) -> None: """skip collect progress report when working on non-terminals. Similar to pytest-dev/pytest#1397 @@ -1081,7 +1087,7 @@ def test_color_yes_collection_on_non_atty(testdir, request): tr = request.config.pluginmanager.getplugin("terminalreporter") if not hasattr(tr, "isatty"): pytest.skip("only valid for newer pytest versions") - testdir.makepyfile( + pytester.makepyfile( """ import pytest @pytest.mark.parametrize('i', range(10)) @@ -1090,34 +1096,34 @@ def test_this(i): """ ) args = ["--color=yes", "-n2"] - result = testdir.runpytest(*args) + result = pytester.runpytest(*args) assert "test session starts" in result.stdout.str() assert "\x1b[1m" in result.stdout.str() assert "gw0 [10] / gw1 [10]" in result.stdout.str() assert "gw0 C / gw1 C" not in result.stdout.str() -def test_without_terminal_plugin(testdir, request): +def test_without_terminal_plugin(pytester, request) -> None: """ No output when terminal plugin is disabled """ - testdir.makepyfile( + pytester.makepyfile( """ def test_1(): pass """ ) - result = testdir.runpytest("-p", "no:terminal", "-n2") + result = pytester.runpytest("-p", "no:terminal", "-n2") assert result.stdout.str() == "" assert result.stderr.str() == "" assert result.ret == 0 -def test_internal_error_with_maxfail(testdir): +def test_internal_error_with_maxfail(pytester: pytest.Pytester) -> None: """ Internal error when using --maxfail option (#62, #65). """ - testdir.makepyfile( + pytester.makepyfile( """ import pytest @@ -1131,33 +1137,33 @@ def test_aaa1(crasher): pass """ ) - result = testdir.runpytest_subprocess("--maxfail=1", "-n1") + result = pytester.runpytest_subprocess("--maxfail=1", "-n1") result.stdout.fnmatch_lines(["* 1 error in *"]) assert "INTERNALERROR" not in result.stderr.str() -def test_internal_errors_propagate_to_controller(testdir): - testdir.makeconftest( +def test_internal_errors_propagate_to_controller(pytester: pytest.Pytester) -> None: + pytester.makeconftest( """ def pytest_collection_modifyitems(): raise RuntimeError("Some runtime error") """ ) - testdir.makepyfile("def test(): pass") - result = testdir.runpytest("-n1") + pytester.makepyfile("def test(): pass") + result = pytester.runpytest("-n1") result.stdout.fnmatch_lines(["*RuntimeError: Some runtime error*"]) class TestLoadScope: - def test_by_module(self, testdir): + def test_by_module(self, pytester: pytest.Pytester) -> None: test_file = """ import pytest @pytest.mark.parametrize('i', range(10)) def test(i): pass """ - testdir.makepyfile(test_a=test_file, test_b=test_file) - result = testdir.runpytest("-n2", "--dist=loadscope", "-v") + pytester.makepyfile(test_a=test_file, test_b=test_file) + result = pytester.runpytest("-n2", "--dist=loadscope", "-v") assert get_workers_and_test_count_by_prefix( "test_a.py::test", result.outlines ) in ({"gw0": 10}, {"gw1": 10}) @@ -1165,8 +1171,8 @@ def test(i): "test_b.py::test", result.outlines ) in ({"gw0": 10}, {"gw1": 10}) - def test_by_class(self, testdir): - testdir.makepyfile( + def test_by_class(self, pytester: pytest.Pytester) -> None: + pytester.makepyfile( test_a=""" import pytest class TestA: @@ -1180,7 +1186,7 @@ def test(self, i): pass """ ) - result = testdir.runpytest("-n2", "--dist=loadscope", "-v") + result = pytester.runpytest("-n2", "--dist=loadscope", "-v") assert get_workers_and_test_count_by_prefix( "test_a.py::TestA", result.outlines ) in ({"gw0": 10}, {"gw1": 10}) @@ -1188,7 +1194,7 @@ def test(self, i): "test_a.py::TestB", result.outlines ) in ({"gw0": 10}, {"gw1": 10}) - def test_module_single_start(self, testdir): + def test_module_single_start(self, pytester: pytest.Pytester) -> None: """Fix test suite never finishing in case all workers start with a single test (#277).""" test_file1 = """ import pytest @@ -1202,8 +1208,8 @@ def test_1(): def test_2(): pass """ - testdir.makepyfile(test_a=test_file1, test_b=test_file1, test_c=test_file2) - result = testdir.runpytest("-n2", "--dist=loadscope", "-v") + pytester.makepyfile(test_a=test_file1, test_b=test_file1, test_c=test_file2) + result = pytester.runpytest("-n2", "--dist=loadscope", "-v") a = get_workers_and_test_count_by_prefix("test_a.py::test", result.outlines) b = get_workers_and_test_count_by_prefix("test_b.py::test", result.outlines) c1 = get_workers_and_test_count_by_prefix("test_c.py::test_1", result.outlines) @@ -1215,7 +1221,7 @@ def test_2(): class TestFileScope: - def test_by_module(self, testdir): + def test_by_module(self, pytester: pytest.Pytester) -> None: test_file = """ import pytest class TestA: @@ -1228,8 +1234,8 @@ class TestB: def test(self, i): pass """ - testdir.makepyfile(test_a=test_file, test_b=test_file) - result = testdir.runpytest("-n2", "--dist=loadfile", "-v") + pytester.makepyfile(test_a=test_file, test_b=test_file) + result = pytester.runpytest("-n2", "--dist=loadfile", "-v") test_a_workers_and_test_count = get_workers_and_test_count_by_prefix( "test_a.py::TestA", result.outlines ) @@ -1254,8 +1260,8 @@ def test(self, i): or test_b_workers_and_test_count in ({"gw0": 0}, {"gw1": 10}) ) - def test_by_class(self, testdir): - testdir.makepyfile( + def test_by_class(self, pytester: pytest.Pytester) -> None: + pytester.makepyfile( test_a=""" import pytest class TestA: @@ -1269,7 +1275,7 @@ def test(self, i): pass """ ) - result = testdir.runpytest("-n2", "--dist=loadfile", "-v") + result = pytester.runpytest("-n2", "--dist=loadfile", "-v") test_a_workers_and_test_count = get_workers_and_test_count_by_prefix( "test_a.py::TestA", result.outlines ) @@ -1294,7 +1300,7 @@ def test(self, i): or test_b_workers_and_test_count in ({"gw0": 0}, {"gw1": 10}) ) - def test_module_single_start(self, testdir): + def test_module_single_start(self, pytester: pytest.Pytester) -> None: """Fix test suite never finishing in case all workers start with a single test (#277).""" test_file1 = """ import pytest @@ -1308,8 +1314,8 @@ def test_1(): def test_2(): pass """ - testdir.makepyfile(test_a=test_file1, test_b=test_file1, test_c=test_file2) - result = testdir.runpytest("-n2", "--dist=loadfile", "-v") + pytester.makepyfile(test_a=test_file1, test_b=test_file1, test_c=test_file2) + result = pytester.runpytest("-n2", "--dist=loadfile", "-v") a = get_workers_and_test_count_by_prefix("test_a.py::test", result.outlines) b = get_workers_and_test_count_by_prefix("test_b.py::test", result.outlines) c1 = get_workers_and_test_count_by_prefix("test_c.py::test_1", result.outlines) @@ -1353,24 +1359,24 @@ def test_c(self): ) @pytest.mark.parametrize("scope", ["each", "load", "loadscope", "loadfile", "no"]) - def test_single_file(self, testdir, scope): - testdir.makepyfile(test_a=self.test_file1) - result = testdir.runpytest("-n2", "--dist=%s" % scope, "-v") + def test_single_file(self, pytester, scope) -> None: + pytester.makepyfile(test_a=self.test_file1) + result = pytester.runpytest("-n2", "--dist=%s" % scope, "-v") result.assert_outcomes(passed=(12 if scope != "each" else 12 * 2)) @pytest.mark.parametrize("scope", ["each", "load", "loadscope", "loadfile", "no"]) - def test_multi_file(self, testdir, scope): - testdir.makepyfile( + def test_multi_file(self, pytester, scope) -> None: + pytester.makepyfile( test_a=self.test_file1, test_b=self.test_file1, test_c=self.test_file1, test_d=self.test_file1, ) - result = testdir.runpytest("-n2", "--dist=%s" % scope, "-v") + result = pytester.runpytest("-n2", "--dist=%s" % scope, "-v") result.assert_outcomes(passed=(48 if scope != "each" else 48 * 2)) -def parse_tests_and_workers_from_output(lines): +def parse_tests_and_workers_from_output(lines: List[str]) -> List[Tuple[str, str, str]]: result = [] for line in lines: # example match: "[gw0] PASSED test_a.py::test[7]" @@ -1391,8 +1397,10 @@ def parse_tests_and_workers_from_output(lines): return result -def get_workers_and_test_count_by_prefix(prefix, lines, expected_status="PASSED"): - result = {} +def get_workers_and_test_count_by_prefix( + prefix: str, lines: List[str], expected_status: str = "PASSED" +) -> Dict[str, int]: + result: Dict[str, int] = {} for worker, status, nodeid in parse_tests_and_workers_from_output(lines): if expected_status == status and nodeid.startswith(prefix): result[worker] = result.get(worker, 0) + 1 @@ -1417,13 +1425,12 @@ def __init__(self): return FakeRequest() - def test_is_xdist_worker(self, fake_request): + def test_is_xdist_worker(self, fake_request) -> None: assert xdist.is_xdist_worker(fake_request) del fake_request.config.workerinput assert not xdist.is_xdist_worker(fake_request) - def test_is_xdist_controller(self, fake_request): - + def test_is_xdist_controller(self, fake_request) -> None: assert not xdist.is_xdist_master(fake_request) assert not xdist.is_xdist_controller(fake_request) @@ -1435,7 +1442,7 @@ def test_is_xdist_controller(self, fake_request): assert not xdist.is_xdist_master(fake_request) assert not xdist.is_xdist_controller(fake_request) - def test_get_xdist_worker_id(self, fake_request): + def test_get_xdist_worker_id(self, fake_request) -> None: assert xdist.get_xdist_worker_id(fake_request) == "gw5" del fake_request.config.workerinput assert xdist.get_xdist_worker_id(fake_request) == "master" diff --git a/testing/conftest.py b/testing/conftest.py index 52f03082..dd7293d2 100644 --- a/testing/conftest.py +++ b/testing/conftest.py @@ -1,12 +1,13 @@ -import py -import pytest import execnet +import pytest +import shutil +from typing import List pytest_plugins = "pytester" @pytest.fixture(autouse=True) -def _divert_atexit(request, monkeypatch): +def _divert_atexit(request, monkeypatch: pytest.MonkeyPatch): import atexit finalizers = [] @@ -23,7 +24,7 @@ def fake_register(func, *args, **kwargs): func(*args, **kwargs) -def pytest_addoption(parser): +def pytest_addoption(parser) -> None: parser.addoption( "--gx", action="append", @@ -33,28 +34,28 @@ def pytest_addoption(parser): @pytest.fixture -def specssh(request): +def specssh(request) -> str: return getspecssh(request.config) # configuration information for tests -def getgspecs(config): +def getgspecs(config) -> List[execnet.XSpec]: return [execnet.XSpec(spec) for spec in config.getvalueorskip("gspecs")] -def getspecssh(config): +def getspecssh(config) -> str: # type: ignore[return] xspecs = getgspecs(config) for spec in xspecs: if spec.ssh: - if not py.path.local.sysfind("ssh"): - py.test.skip("command not found: ssh") + if not shutil.which("ssh"): + pytest.skip("command not found: ssh") return str(spec) - py.test.skip("need '--gx ssh=...'") + pytest.skip("need '--gx ssh=...'") -def getsocketspec(config): +def getsocketspec(config) -> execnet.XSpec: xspecs = getgspecs(config) for spec in xspecs: if spec.socket: return spec - py.test.skip("need '--gx socket=...'") + pytest.skip("need '--gx socket=...'") diff --git a/testing/test_dsession.py b/testing/test_dsession.py index b015c75e..464045e7 100644 --- a/testing/test_dsession.py +++ b/testing/test_dsession.py @@ -1,59 +1,44 @@ from xdist.dsession import DSession, get_default_max_worker_restart from xdist.report import report_collection_diff from xdist.scheduler import EachScheduling, LoadScheduling +from typing import Optional -import py import pytest import execnet -XSpec = execnet.XSpec - - -def run(item, node, excinfo=None): - runner = item.config.pluginmanager.getplugin("runner") - rep = runner.ItemTestReport(item=item, excinfo=excinfo, when="call") - rep.node = node - return rep - class MockGateway: - _count = 0 - - def __init__(self): + def __init__(self) -> None: + self._count = 0 self.id = str(self._count) self._count += 1 class MockNode: - def __init__(self): - self.sent = [] + def __init__(self) -> None: + self.sent = [] # type: ignore[var-annotated] self.gateway = MockGateway() self._shutdown = False - def send_runtest_some(self, indices): + def send_runtest_some(self, indices) -> None: self.sent.extend(indices) - def send_runtest_all(self): + def send_runtest_all(self) -> None: self.sent.append("ALL") - def shutdown(self): + def shutdown(self) -> None: self._shutdown = True @property - def shutting_down(self): + def shutting_down(self) -> bool: return self._shutdown -def dumpqueue(queue): - while queue.qsize(): - print(queue.get()) - - class TestEachScheduling: - def test_schedule_load_simple(self, testdir): + def test_schedule_load_simple(self, pytester: pytest.Pytester) -> None: node1 = MockNode() node2 = MockNode() - config = testdir.parseconfig("--tx=2*popen") + config = pytester.parseconfig("--tx=2*popen") sched = EachScheduling(config) sched.add_node(node1) sched.add_node(node2) @@ -74,9 +59,9 @@ def test_schedule_load_simple(self, testdir): sched.mark_test_complete(node2, 0) assert sched.tests_finished - def test_schedule_remove_node(self, testdir): + def test_schedule_remove_node(self, pytester: pytest.Pytester) -> None: node1 = MockNode() - config = testdir.parseconfig("--tx=popen") + config = pytester.parseconfig("--tx=popen") sched = EachScheduling(config) sched.add_node(node1) collection = ["a.py::test_1"] @@ -93,8 +78,8 @@ def test_schedule_remove_node(self, testdir): class TestLoadScheduling: - def test_schedule_load_simple(self, testdir): - config = testdir.parseconfig("--tx=2*popen") + def test_schedule_load_simple(self, pytester: pytest.Pytester) -> None: + config = pytester.parseconfig("--tx=2*popen") sched = LoadScheduling(config) sched.add_node(MockNode()) sched.add_node(MockNode()) @@ -117,8 +102,8 @@ def test_schedule_load_simple(self, testdir): sched.mark_test_complete(node1, node1.sent[0]) assert sched.tests_finished - def test_schedule_batch_size(self, testdir): - config = testdir.parseconfig("--tx=2*popen") + def test_schedule_batch_size(self, pytester: pytest.Pytester) -> None: + config = pytester.parseconfig("--tx=2*popen") sched = LoadScheduling(config) sched.add_node(MockNode()) sched.add_node(MockNode()) @@ -144,8 +129,8 @@ def test_schedule_batch_size(self, testdir): assert node1.sent == [0, 2, 4, 5] assert not sched.pending - def test_schedule_fewer_tests_than_nodes(self, testdir): - config = testdir.parseconfig("--tx=2*popen") + def test_schedule_fewer_tests_than_nodes(self, pytester: pytest.Pytester) -> None: + config = pytester.parseconfig("--tx=2*popen") sched = LoadScheduling(config) sched.add_node(MockNode()) sched.add_node(MockNode()) @@ -164,8 +149,10 @@ def test_schedule_fewer_tests_than_nodes(self, testdir): assert sent3 == [] assert not sched.pending - def test_schedule_fewer_than_two_tests_per_node(self, testdir): - config = testdir.parseconfig("--tx=2*popen") + def test_schedule_fewer_than_two_tests_per_node( + self, pytester: pytest.Pytester + ) -> None: + config = pytester.parseconfig("--tx=2*popen") sched = LoadScheduling(config) sched.add_node(MockNode()) sched.add_node(MockNode()) @@ -184,9 +171,9 @@ def test_schedule_fewer_than_two_tests_per_node(self, testdir): assert sent3 == [2] assert not sched.pending - def test_add_remove_node(self, testdir): + def test_add_remove_node(self, pytester: pytest.Pytester) -> None: node = MockNode() - config = testdir.parseconfig("--tx=popen") + config = pytester.parseconfig("--tx=popen") sched = LoadScheduling(config) sched.add_node(node) collection = ["test_file.py::test_func"] @@ -197,7 +184,7 @@ def test_add_remove_node(self, testdir): crashitem = sched.remove_node(node) assert crashitem == collection[0] - def test_different_tests_collected(self, testdir): + def test_different_tests_collected(self, pytester: pytest.Pytester) -> None: """ Test that LoadScheduling is reporting collection errors when different test ids are collected by workers. @@ -215,7 +202,7 @@ def pytest_collectreport(self, report): self.reports.append(report) collect_hook = CollectHook() - config = testdir.parseconfig("--tx=2*popen") + config = pytester.parseconfig("--tx=2*popen") config.pluginmanager.register(collect_hook, "collect_hook") node1 = MockNode() node2 = MockNode() @@ -231,9 +218,9 @@ def pytest_collectreport(self, report): class TestDistReporter: - @py.test.mark.xfail - def test_rsync_printing(self, testdir, linecomp): - config = testdir.parseconfig() + @pytest.mark.xfail + def test_rsync_printing(self, pytester: pytest.Pytester, linecomp) -> None: + config = pytester.parseconfig() from _pytest.pytest_terminal import TerminalReporter rep = TerminalReporter(config, file=linecomp.stringio) @@ -258,21 +245,21 @@ class gw2: # linecomp.assert_contains_lines([ # "*X1*popen*xyz*2.5*" # ]) - dsession.pytest_xdist_rsyncstart(source="hello", gateways=[gw1, gw2]) + dsession.pytest_xdist_rsyncstart(source="hello", gateways=[gw1, gw2]) # type: ignore[attr-defined] linecomp.assert_contains_lines(["[X1,X2] rsyncing: hello"]) -def test_report_collection_diff_equal(): +def test_report_collection_diff_equal() -> None: """Test reporting of equal collections.""" from_collection = to_collection = ["aaa", "bbb", "ccc"] assert report_collection_diff(from_collection, to_collection, 1, 2) is None -def test_default_max_worker_restart(): +def test_default_max_worker_restart() -> None: class config: class option: - maxworkerrestart = None - numprocesses = 0 + maxworkerrestart: Optional[str] = None + numprocesses: int = 0 assert get_default_max_worker_restart(config) is None @@ -286,7 +273,7 @@ class option: assert get_default_max_worker_restart(config) == 0 -def test_report_collection_diff_different(): +def test_report_collection_diff_different() -> None: """Test reporting of different collections.""" from_collection = ["aaa", "bbb", "ccc", "YYY"] to_collection = ["aZa", "bbb", "XXX", "ccc"] @@ -311,8 +298,8 @@ def test_report_collection_diff_different(): @pytest.mark.xfail(reason="duplicate test ids not supported yet") -def test_pytest_issue419(testdir): - testdir.makepyfile( +def test_pytest_issue419(pytester: pytest.Pytester) -> None: + pytester.makepyfile( """ import pytest @@ -321,6 +308,6 @@ def test_2011_table(birth_year): pass """ ) - reprec = testdir.inline_run("-n1") + reprec = pytester.inline_run("-n1") reprec.assertoutcome(passed=2) assert 0 diff --git a/testing/test_looponfail.py b/testing/test_looponfail.py index 4b69a85f..02a1f592 100644 --- a/testing/test_looponfail.py +++ b/testing/test_looponfail.py @@ -1,90 +1,106 @@ import py import pytest -from pkg_resources import parse_version +import shutil +import textwrap +from pathlib import Path from xdist.looponfail import RemoteControl from xdist.looponfail import StatRecorder +PYTEST_GTE_7 = hasattr(pytest, "version_tuple") and pytest.version_tuple >= (7, 0) # type: ignore[attr-defined] + + class TestStatRecorder: - def test_filechange(self, tmpdir): - tmp = tmpdir - hello = tmp.ensure("hello.py") - sd = StatRecorder([tmp]) + def test_filechange(self, tmp_path: Path) -> None: + tmp = tmp_path + hello = tmp / "hello.py" + hello.touch() + sd = StatRecorder([py.path.local(tmp)]) changed = sd.check() assert not changed - hello.write("world") + hello.write_text("world") changed = sd.check() assert changed - (hello + "c").write("hello") + hello.with_suffix(".pyc").write_text("hello") changed = sd.check() assert not changed - p = tmp.ensure("new.py") + p = tmp / "new.py" + p.touch() changed = sd.check() assert changed - p.remove() + p.unlink() changed = sd.check() assert changed - tmp.join("a", "b", "c.py").ensure() + tmp.joinpath("a", "b").mkdir(parents=True) + tmp.joinpath("a", "b", "c.py").touch() changed = sd.check() assert changed - tmp.join("a", "c.txt").ensure() + tmp.joinpath("a", "c.txt").touch() changed = sd.check() assert changed changed = sd.check() assert not changed - tmp.join("a").remove() + shutil.rmtree(str(tmp.joinpath("a"))) changed = sd.check() assert changed - def test_dirchange(self, tmpdir): - tmp = tmpdir - tmp.ensure("dir", "hello.py") - sd = StatRecorder([tmp]) - assert not sd.fil(tmp.join("dir")) - - def test_filechange_deletion_race(self, tmpdir, monkeypatch): - tmp = tmpdir - sd = StatRecorder([tmp]) + def test_dirchange(self, tmp_path: Path) -> None: + tmp = tmp_path + tmp.joinpath("dir").mkdir() + tmp.joinpath("dir", "hello.py").touch() + sd = StatRecorder([py.path.local(tmp)]) + assert not sd.fil(py.path.local(tmp / "dir")) + + def test_filechange_deletion_race( + self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch + ) -> None: + tmp = tmp_path + pytmp = py.path.local(tmp) + sd = StatRecorder([pytmp]) changed = sd.check() assert not changed - p = tmp.ensure("new.py") + p = tmp.joinpath("new.py") + p.touch() changed = sd.check() assert changed - p.remove() + p.unlink() # make check()'s visit() call return our just removed # path as if we were in a race condition - monkeypatch.setattr(tmp, "visit", lambda *args: [p]) + monkeypatch.setattr(pytmp, "visit", lambda *args: [py.path.local(p)]) changed = sd.check() assert changed - def test_pycremoval(self, tmpdir): - tmp = tmpdir - hello = tmp.ensure("hello.py") - sd = StatRecorder([tmp]) + def test_pycremoval(self, tmp_path: Path) -> None: + tmp = tmp_path + hello = tmp / "hello.py" + hello.touch() + sd = StatRecorder([py.path.local(tmp)]) changed = sd.check() assert not changed - pycfile = hello + "c" - pycfile.ensure() - hello.write("world") + pycfile = hello.with_suffix(".pyc") + pycfile.touch() + hello.write_text("world") changed = sd.check() assert changed - assert not pycfile.check() + assert not pycfile.exists() - def test_waitonchange(self, tmpdir, monkeypatch): - tmp = tmpdir - sd = StatRecorder([tmp]) + def test_waitonchange( + self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch + ) -> None: + tmp = tmp_path + sd = StatRecorder([py.path.local(tmp)]) ret_values = [True, False] monkeypatch.setattr(StatRecorder, "check", lambda self: ret_values.pop()) @@ -93,63 +109,74 @@ def test_waitonchange(self, tmpdir, monkeypatch): class TestRemoteControl: - def test_nofailures(self, testdir): - item = testdir.getitem("def test_func(): pass\n") + def test_nofailures(self, pytester: pytest.Pytester) -> None: + item = pytester.getitem("def test_func(): pass\n") control = RemoteControl(item.config) control.setup() topdir, failures = control.runsession()[:2] assert not failures - def test_failures_somewhere(self, testdir): - item = testdir.getitem("def test_func():\n assert 0\n") + def test_failures_somewhere(self, pytester: pytest.Pytester) -> None: + item = pytester.getitem("def test_func():\n assert 0\n") control = RemoteControl(item.config) control.setup() failures = control.runsession() assert failures control.setup() - item.fspath.write("def test_func():\n assert 1\n") - removepyc(item.fspath) + item_path = item.path if PYTEST_GTE_7 else Path(item.fspath) # type: ignore[attr-defined] + item_path.write_text("def test_func():\n assert 1\n") + removepyc(item_path) topdir, failures = control.runsession()[:2] assert not failures - def test_failure_change(self, testdir): - modcol = testdir.getitem( - """ - def test_func(): - assert 0 - """ + def test_failure_change(self, pytester: pytest.Pytester) -> None: + modcol = pytester.getitem( + textwrap.dedent( + """ + def test_func(): + assert 0 + """ + ) ) control = RemoteControl(modcol.config) control.loop_once() assert control.failures - modcol.fspath.write( - py.code.Source( + modcol_path = modcol.path if PYTEST_GTE_7 else Path(modcol.fspath) # type: ignore[attr-defined] + modcol_path.write_text( + textwrap.dedent( + """ + def test_func(): + assert 1 + def test_new(): + assert 0 """ - def test_func(): - assert 1 - def test_new(): - assert 0 - """ ) ) - removepyc(modcol.fspath) + removepyc(modcol_path) control.loop_once() assert not control.failures control.loop_once() assert control.failures assert str(control.failures).find("test_new") != -1 - def test_failure_subdir_no_init(self, testdir): - modcol = testdir.getitem( - """ - def test_func(): - assert 0 - """ + def test_failure_subdir_no_init( + self, pytester: pytest.Pytester, monkeypatch: pytest.MonkeyPatch + ) -> None: + modcol = pytester.getitem( + textwrap.dedent( + """ + def test_func(): + assert 0 + """ + ) ) - parent = modcol.fspath.dirpath().dirpath() - parent.chdir() + if PYTEST_GTE_7: + parent = modcol.path.parent.parent # type: ignore[attr-defined] + else: + parent = Path(modcol.fspath.dirpath().dirpath()) + monkeypatch.chdir(parent) modcol.config.args = [ - py.path.local(x).relto(parent) for x in modcol.config.args + str(Path(x).relative_to(parent)) for x in modcol.config.args ] control = RemoteControl(modcol.config) control.loop_once() @@ -159,57 +186,63 @@ def test_func(): class TestLooponFailing: - def test_looponfail_from_fail_to_ok(self, testdir): - modcol = testdir.getmodulecol( - """ - def test_one(): - x = 0 - assert x == 1 - def test_two(): - assert 1 - """ + def test_looponfail_from_fail_to_ok(self, pytester: pytest.Pytester) -> None: + modcol = pytester.getmodulecol( + textwrap.dedent( + """ + def test_one(): + x = 0 + assert x == 1 + def test_two(): + assert 1 + """ + ) ) remotecontrol = RemoteControl(modcol.config) remotecontrol.loop_once() assert len(remotecontrol.failures) == 1 - modcol.fspath.write( - py.code.Source( + modcol_path = modcol.path if PYTEST_GTE_7 else Path(modcol.fspath) + modcol_path.write_text( + textwrap.dedent( + """ + def test_one(): + assert 1 + def test_two(): + assert 1 """ - def test_one(): - assert 1 - def test_two(): - assert 1 - """ ) ) - removepyc(modcol.fspath) + removepyc(modcol_path) remotecontrol.loop_once() assert not remotecontrol.failures - def test_looponfail_from_one_to_two_tests(self, testdir): - modcol = testdir.getmodulecol( - """ - def test_one(): - assert 0 - """ + def test_looponfail_from_one_to_two_tests(self, pytester: pytest.Pytester) -> None: + modcol = pytester.getmodulecol( + textwrap.dedent( + """ + def test_one(): + assert 0 + """ + ) ) remotecontrol = RemoteControl(modcol.config) remotecontrol.loop_once() assert len(remotecontrol.failures) == 1 assert "test_one" in remotecontrol.failures[0] - modcol.fspath.write( - py.code.Source( + modcol_path = modcol.path if PYTEST_GTE_7 else Path(modcol.fspath) + modcol_path.write_text( + textwrap.dedent( + """ + def test_one(): + assert 1 # passes now + def test_two(): + assert 0 # new and fails """ - def test_one(): - assert 1 # passes now - def test_two(): - assert 0 # new and fails - """ ) ) - removepyc(modcol.fspath) + removepyc(modcol_path) remotecontrol.loop_once() assert len(remotecontrol.failures) == 0 remotecontrol.loop_once() @@ -217,47 +250,49 @@ def test_two(): assert "test_one" not in remotecontrol.failures[0] assert "test_two" in remotecontrol.failures[0] - @pytest.mark.xfail( - parse_version(pytest.__version__) >= parse_version("3.1"), - reason="broken by pytest 3.1+", - strict=True, - ) - def test_looponfail_removed_test(self, testdir): - modcol = testdir.getmodulecol( - """ - def test_one(): - assert 0 - def test_two(): - assert 0 - """ + @pytest.mark.xfail(reason="broken by pytest 3.1+", strict=True) + def test_looponfail_removed_test(self, pytester: pytest.Pytester) -> None: + modcol = pytester.getmodulecol( + textwrap.dedent( + """ + def test_one(): + assert 0 + def test_two(): + assert 0 + """ + ) ) remotecontrol = RemoteControl(modcol.config) remotecontrol.loop_once() assert len(remotecontrol.failures) == 2 - modcol.fspath.write( - py.code.Source( + modcol.path.write_text( + textwrap.dedent( + """ + def test_xxx(): # renamed test + assert 0 + def test_two(): + assert 1 # pass now """ - def test_xxx(): # renamed test - assert 0 - def test_two(): - assert 1 # pass now - """ ) ) - removepyc(modcol.fspath) + removepyc(modcol.path) remotecontrol.loop_once() assert len(remotecontrol.failures) == 0 remotecontrol.loop_once() assert len(remotecontrol.failures) == 1 - def test_looponfail_multiple_errors(self, testdir, monkeypatch): - modcol = testdir.getmodulecol( - """ - def test_one(): - assert 0 - """ + def test_looponfail_multiple_errors( + self, pytester: pytest.Pytester, monkeypatch: pytest.MonkeyPatch + ) -> None: + modcol = pytester.getmodulecol( + textwrap.dedent( + """ + def test_one(): + assert 0 + """ + ) ) remotecontrol = RemoteControl(modcol.config) orig_runsession = remotecontrol.runsession @@ -274,55 +309,59 @@ def runsession_dups(): class TestFunctional: - def test_fail_to_ok(self, testdir): - p = testdir.makepyfile( - """ - def test_one(): - x = 0 - assert x == 1 - """ + def test_fail_to_ok(self, pytester: pytest.Pytester) -> None: + p = pytester.makepyfile( + textwrap.dedent( + """ + def test_one(): + x = 0 + assert x == 1 + """ + ) ) - # p = testdir.mkdir("sub").join(p1.basename) + # p = pytester.mkdir("sub").join(p1.basename) # p1.move(p) - child = testdir.spawn_pytest("-f %s --traceconfig" % p, expect_timeout=30.0) + child = pytester.spawn_pytest("-f %s --traceconfig" % p, expect_timeout=30.0) child.expect("def test_one") child.expect("x == 1") child.expect("1 failed") child.expect("### LOOPONFAILING ####") child.expect("waiting for changes") - p.write( - py.code.Source( + p.write_text( + textwrap.dedent( """ - def test_one(): - x = 1 - assert x == 1 - """ - ) + def test_one(): + x = 1 + assert x == 1 + """ + ), ) child.expect(".*1 passed.*") child.kill(15) - def test_xfail_passes(self, testdir): - p = testdir.makepyfile( - """ - import py - @py.test.mark.xfail - def test_one(): - pass - """ + def test_xfail_passes(self, pytester: pytest.Pytester) -> None: + p = pytester.makepyfile( + textwrap.dedent( + """ + import pytest + @pytest.mark.xfail + def test_one(): + pass + """ + ) ) - child = testdir.spawn_pytest("-f %s" % p, expect_timeout=30.0) + child = pytester.spawn_pytest("-f %s" % p, expect_timeout=30.0) child.expect("1 xpass") # child.expect("### LOOPONFAILING ####") child.expect("waiting for changes") child.kill(15) -def removepyc(path): +def removepyc(path: Path) -> None: # XXX damn those pyc files - pyc = path + "c" - if pyc.check(): - pyc.remove() - c = path.dirpath("__pycache__") - if c.check(): - c.remove() + pyc = path.with_suffix(".pyc") + if pyc.exists(): + pyc.unlink() + c = path.parent / "__pycache__" + if c.exists(): + shutil.rmtree(c) diff --git a/testing/test_newhooks.py b/testing/test_newhooks.py index d2c28788..012f1ea7 100644 --- a/testing/test_newhooks.py +++ b/testing/test_newhooks.py @@ -3,8 +3,8 @@ class TestHooks: @pytest.fixture(autouse=True) - def create_test_file(self, testdir): - testdir.makepyfile( + def create_test_file(self, pytester: pytest.Pytester) -> None: + pytester.makepyfile( """ import os def test_a(): pass @@ -13,11 +13,11 @@ def test_c(): pass """ ) - def test_runtest_logreport(self, testdir): + def test_runtest_logreport(self, pytester: pytest.Pytester) -> None: """Test that log reports from pytest_runtest_logreport when running with xdist contain "node", "nodeid", "worker_id", and "testrun_uid" attributes. (#8) """ - testdir.makeconftest( + pytester.makeconftest( """ def pytest_runtest_logreport(report): if hasattr(report, 'node'): @@ -35,7 +35,7 @@ def pytest_runtest_logreport(report): % (report.nodeid, report.worker_id, report.testrun_uid)) """ ) - res = testdir.runpytest("-n1", "-s") + res = pytester.runpytest("-n1", "-s") res.stdout.fnmatch_lines( [ "*HOOK: test_runtest_logreport.py::test_a gw0 *", @@ -45,9 +45,9 @@ def pytest_runtest_logreport(report): ] ) - def test_node_collection_finished(self, testdir): + def test_node_collection_finished(self, pytester: pytest.Pytester) -> None: """Test pytest_xdist_node_collection_finished hook (#8).""" - testdir.makeconftest( + pytester.makeconftest( """ def pytest_xdist_node_collection_finished(node, ids): workerid = node.workerinput['workerid'] @@ -55,7 +55,7 @@ def pytest_xdist_node_collection_finished(node, ids): print("HOOK: %s %s" % (workerid, ', '.join(stripped_ids))) """ ) - res = testdir.runpytest("-n2", "-s") + res = pytester.runpytest("-n2", "-s") res.stdout.fnmatch_lines_random( ["*HOOK: gw0 test_a, test_b, test_c", "*HOOK: gw1 test_a, test_b, test_c"] ) @@ -64,8 +64,8 @@ def pytest_xdist_node_collection_finished(node, ids): class TestCrashItem: @pytest.fixture(autouse=True) - def create_test_file(self, testdir): - testdir.makepyfile( + def create_test_file(self, pytester: pytest.Pytester) -> None: + pytester.makepyfile( """ import os def test_a(): pass @@ -75,9 +75,9 @@ def test_d(): pass """ ) - def test_handlecrashitem(self, testdir): + def test_handlecrashitem(self, pytester: pytest.Pytester) -> None: """Test pytest_handlecrashitem hook.""" - testdir.makeconftest( + pytester.makeconftest( """ test_runs = 0 @@ -91,6 +91,6 @@ def pytest_handlecrashitem(crashitem, report, sched): print("HOOK: pytest_handlecrashitem") """ ) - res = testdir.runpytest("-n2", "-s") + res = pytester.runpytest("-n2", "-s") res.stdout.fnmatch_lines_random(["*HOOK: pytest_handlecrashitem"]) res.stdout.fnmatch_lines(["*3 passed*"]) diff --git a/testing/test_plugin.py b/testing/test_plugin.py index 58706767..e50c0cd9 100644 --- a/testing/test_plugin.py +++ b/testing/test_plugin.py @@ -1,44 +1,46 @@ from contextlib import suppress +from pathlib import Path -import py import execnet from xdist.workermanage import NodeManager import pytest -def test_dist_incompatibility_messages(testdir): - result = testdir.runpytest("--pdb", "--looponfail") +def test_dist_incompatibility_messages(pytester: pytest.Pytester) -> None: + result = pytester.runpytest("--pdb", "--looponfail") assert result.ret != 0 - result = testdir.runpytest("--pdb", "-n", "3") + result = pytester.runpytest("--pdb", "-n", "3") assert result.ret != 0 assert "incompatible" in result.stderr.str() - result = testdir.runpytest("--pdb", "-d", "--tx", "popen") + result = pytester.runpytest("--pdb", "-d", "--tx", "popen") assert result.ret != 0 assert "incompatible" in result.stderr.str() -def test_dist_options(testdir): +def test_dist_options(pytester: pytest.Pytester) -> None: from xdist.plugin import pytest_cmdline_main as check_options - config = testdir.parseconfigure("-n 2") + config = pytester.parseconfigure("-n 2") check_options(config) assert config.option.dist == "load" assert config.option.tx == ["popen"] * 2 - config = testdir.parseconfigure("--numprocesses", "2") + config = pytester.parseconfigure("--numprocesses", "2") check_options(config) assert config.option.dist == "load" assert config.option.tx == ["popen"] * 2 - config = testdir.parseconfigure("--numprocesses", "3", "--maxprocesses", "2") + config = pytester.parseconfigure("--numprocesses", "3", "--maxprocesses", "2") check_options(config) assert config.option.dist == "load" assert config.option.tx == ["popen"] * 2 - config = testdir.parseconfigure("-d") + config = pytester.parseconfigure("-d") check_options(config) assert config.option.dist == "load" -def test_auto_detect_cpus(testdir, monkeypatch): +def test_auto_detect_cpus( + pytester: pytest.Pytester, monkeypatch: pytest.MonkeyPatch +) -> None: import os from xdist.plugin import pytest_cmdline_main as check_options @@ -56,20 +58,20 @@ def test_auto_detect_cpus(testdir, monkeypatch): monkeypatch.setattr(multiprocessing, "cpu_count", lambda: 99) - config = testdir.parseconfigure("-n2") + config = pytester.parseconfigure("-n2") assert config.getoption("numprocesses") == 2 - config = testdir.parseconfigure("-nauto") + config = pytester.parseconfigure("-nauto") check_options(config) assert config.getoption("numprocesses") == 99 - config = testdir.parseconfigure("-nauto", "--pdb") + config = pytester.parseconfigure("-nauto", "--pdb") check_options(config) assert config.getoption("usepdb") assert config.getoption("numprocesses") == 0 assert config.getoption("dist") == "no" - config = testdir.parseconfigure("-nlogical", "--pdb") + config = pytester.parseconfigure("-nlogical", "--pdb") check_options(config) assert config.getoption("usepdb") assert config.getoption("numprocesses") == 0 @@ -77,91 +79,95 @@ def test_auto_detect_cpus(testdir, monkeypatch): monkeypatch.delattr(os, "sched_getaffinity", raising=False) monkeypatch.setenv("TRAVIS", "true") - config = testdir.parseconfigure("-nauto") + config = pytester.parseconfigure("-nauto") check_options(config) assert config.getoption("numprocesses") == 2 -def test_auto_detect_cpus_psutil(testdir, monkeypatch): +def test_auto_detect_cpus_psutil( + pytester: pytest.Pytester, monkeypatch: pytest.MonkeyPatch +) -> None: from xdist.plugin import pytest_cmdline_main as check_options psutil = pytest.importorskip("psutil") monkeypatch.setattr(psutil, "cpu_count", lambda logical=True: 84 if logical else 42) - config = testdir.parseconfigure("-nauto") + config = pytester.parseconfigure("-nauto") check_options(config) assert config.getoption("numprocesses") == 42 - config = testdir.parseconfigure("-nlogical") + config = pytester.parseconfigure("-nlogical") check_options(config) assert config.getoption("numprocesses") == 84 -def test_hook_auto_num_workers(testdir, monkeypatch): +def test_hook_auto_num_workers( + pytester: pytest.Pytester, monkeypatch: pytest.MonkeyPatch +) -> None: from xdist.plugin import pytest_cmdline_main as check_options - testdir.makeconftest( + pytester.makeconftest( """ def pytest_xdist_auto_num_workers(): return 42 """ ) - config = testdir.parseconfigure("-nauto") + config = pytester.parseconfigure("-nauto") check_options(config) assert config.getoption("numprocesses") == 42 - config = testdir.parseconfigure("-nlogical") + config = pytester.parseconfigure("-nlogical") check_options(config) assert config.getoption("numprocesses") == 42 -def test_boxed_with_collect_only(testdir): +def test_boxed_with_collect_only(pytester: pytest.Pytester) -> None: from xdist.plugin import pytest_cmdline_main as check_options - config = testdir.parseconfigure("-n1", "--boxed") + config = pytester.parseconfigure("-n1", "--boxed") check_options(config) assert config.option.forked - config = testdir.parseconfigure("-n1", "--collect-only") + config = pytester.parseconfigure("-n1", "--collect-only") check_options(config) assert not config.option.forked - config = testdir.parseconfigure("-n1", "--boxed", "--collect-only") + config = pytester.parseconfigure("-n1", "--boxed", "--collect-only") check_options(config) assert config.option.forked -def test_dsession_with_collect_only(testdir): +def test_dsession_with_collect_only(pytester: pytest.Pytester) -> None: from xdist.plugin import pytest_cmdline_main as check_options from xdist.plugin import pytest_configure as configure - config = testdir.parseconfigure("-n1") + config = pytester.parseconfigure("-n1") check_options(config) configure(config) assert config.pluginmanager.hasplugin("dsession") - config = testdir.parseconfigure("-n1", "--collect-only") + config = pytester.parseconfigure("-n1", "--collect-only") check_options(config) configure(config) assert not config.pluginmanager.hasplugin("dsession") -def test_testrunuid_provided(testdir): - config = testdir.parseconfigure("--testrunuid", "test123", "--tx=popen") +def test_testrunuid_provided(pytester: pytest.Pytester) -> None: + config = pytester.parseconfigure("--testrunuid", "test123", "--tx=popen") nm = NodeManager(config) assert nm.testrunuid == "test123" -def test_testrunuid_generated(testdir): - config = testdir.parseconfigure("--tx=popen") +def test_testrunuid_generated(pytester: pytest.Pytester) -> None: + config = pytester.parseconfigure("--tx=popen") nm = NodeManager(config) assert len(nm.testrunuid) == 32 class TestDistOptions: - def test_getxspecs(self, testdir): - config = testdir.parseconfigure("--tx=popen", "--tx", "ssh=xyz") + def test_getxspecs(self, pytester: pytest.Pytester) -> None: + config = pytester.parseconfigure("--tx=popen", "--tx", "ssh=xyz") nodemanager = NodeManager(config) xspecs = nodemanager._getxspecs() assert len(xspecs) == 2 @@ -169,39 +175,39 @@ def test_getxspecs(self, testdir): assert xspecs[0].popen assert xspecs[1].ssh == "xyz" - def test_xspecs_multiplied(self, testdir): - config = testdir.parseconfigure("--tx=3*popen") + def test_xspecs_multiplied(self, pytester: pytest.Pytester) -> None: + config = pytester.parseconfigure("--tx=3*popen") xspecs = NodeManager(config)._getxspecs() assert len(xspecs) == 3 assert xspecs[1].popen - def test_getrsyncdirs(self, testdir): - config = testdir.parseconfigure("--rsyncdir=" + str(testdir.tmpdir)) + def test_getrsyncdirs(self, pytester: pytest.Pytester) -> None: + config = pytester.parseconfigure("--rsyncdir=" + str(pytester.path)) nm = NodeManager(config, specs=[execnet.XSpec("popen")]) assert not nm._getrsyncdirs() nm = NodeManager(config, specs=[execnet.XSpec("popen//chdir=qwe")]) assert nm.roots - assert testdir.tmpdir in nm.roots + assert pytester.path in nm.roots - def test_getrsyncignore(self, testdir): - config = testdir.parseconfigure("--rsyncignore=fo*") + def test_getrsyncignore(self, pytester: pytest.Pytester) -> None: + config = pytester.parseconfigure("--rsyncignore=fo*") nm = NodeManager(config, specs=[execnet.XSpec("popen//chdir=qwe")]) assert "fo*" in nm.rsyncoptions["ignores"] - def test_getrsyncdirs_with_conftest(self, testdir): - p = py.path.local() - for bn in "x y z".split(): - p.mkdir(bn) - testdir.makeini( + def test_getrsyncdirs_with_conftest(self, pytester: pytest.Pytester) -> None: + p = Path.cwd() + for bn in ("x", "y", "z"): + p.joinpath(bn).mkdir() + pytester.makeini( """ [pytest] rsyncdirs= x """ ) - config = testdir.parseconfigure(testdir.tmpdir, "--rsyncdir=y", "--rsyncdir=z") + config = pytester.parseconfigure(pytester.path, "--rsyncdir=y", "--rsyncdir=z") nm = NodeManager(config, specs=[execnet.XSpec("popen//chdir=xyz")]) roots = nm._getrsyncdirs() # assert len(roots) == 3 + 1 # pylib - assert py.path.local("y") in roots - assert py.path.local("z") in roots - assert testdir.tmpdir.join("x") in roots + assert Path("y").resolve() in roots + assert Path("z").resolve() in roots + assert pytester.path.joinpath("x") in roots diff --git a/testing/test_remote.py b/testing/test_remote.py index 31a6a2ae..348febc5 100644 --- a/testing/test_remote.py +++ b/testing/test_remote.py @@ -1,5 +1,5 @@ -import py import pprint +import py import pytest import sys import uuid @@ -32,18 +32,16 @@ def __str__(self): class WorkerSetup: use_callback = False - def __init__(self, request, testdir): + def __init__(self, request, pytester: pytest.Pytester) -> None: self.request = request - self.testdir = testdir - self.events = Queue() + self.pytester = pytester + self.events = Queue() # type: ignore[var-annotated] - def setup( - self, - ): - self.testdir.chdir() + def setup(self) -> None: + self.pytester.chdir() # import os ; os.environ['EXECNET_DEBUG'] = "2" self.gateway = execnet.makegateway() - self.config = config = self.testdir.parseconfigure() + self.config = config = self.pytester.parseconfigure() putevent = self.use_callback and self.events.put or None class DummyMananger: @@ -70,15 +68,15 @@ def sendcommand(self, name, **kwargs): @pytest.fixture -def worker(request, testdir): - return WorkerSetup(request, testdir) +def worker(request, pytester: pytest.Pytester) -> WorkerSetup: + return WorkerSetup(request, pytester) @pytest.mark.xfail(reason="#59") -def test_remoteinitconfig(testdir): +def test_remoteinitconfig(pytester: pytest.Pytester) -> None: from xdist.remote import remote_initconfig - config1 = testdir.parseconfig() + config1 = pytester.parseconfig() config2 = remote_initconfig(config1.option.__dict__, config1.args) assert config2.option.__dict__ == config1.option.__dict__ assert config2.pluginmanager.getplugin("terminal") in (-1, None) @@ -94,8 +92,10 @@ def unserialize(data): return unserialize - def test_basic_collect_and_runtests(self, worker, unserialize_report): - worker.testdir.makepyfile( + def test_basic_collect_and_runtests( + self, worker: WorkerSetup, unserialize_report + ) -> None: + worker.pytester.makepyfile( """ def test_func(): pass @@ -108,7 +108,7 @@ def test_func(): assert ev.name == "collectionstart" assert not ev.kwargs ev = worker.popevent("collectionfinish") - assert ev.kwargs["topdir"] == worker.testdir.tmpdir + assert ev.kwargs["topdir"] == py.path.local(worker.pytester.path) ids = ev.kwargs["ids"] assert len(ids) == 1 worker.sendcommand("runtests", indices=list(range(len(ids)))) @@ -126,8 +126,8 @@ def test_func(): ev = worker.popevent("workerfinished") assert "workeroutput" in ev.kwargs - def test_remote_collect_skip(self, worker, unserialize_report): - worker.testdir.makepyfile( + def test_remote_collect_skip(self, worker: WorkerSetup, unserialize_report) -> None: + worker.pytester.makepyfile( """ import pytest pytest.skip("hello", allow_module_level=True) @@ -144,8 +144,8 @@ def test_remote_collect_skip(self, worker, unserialize_report): ev = worker.popevent("collectionfinish") assert not ev.kwargs["ids"] - def test_remote_collect_fail(self, worker, unserialize_report): - worker.testdir.makepyfile("""aasd qwe""") + def test_remote_collect_fail(self, worker: WorkerSetup, unserialize_report) -> None: + worker.pytester.makepyfile("""aasd qwe""") worker.setup() ev = worker.popevent("collectionstart") assert not ev.kwargs @@ -156,8 +156,8 @@ def test_remote_collect_fail(self, worker, unserialize_report): ev = worker.popevent("collectionfinish") assert not ev.kwargs["ids"] - def test_runtests_all(self, worker, unserialize_report): - worker.testdir.makepyfile( + def test_runtests_all(self, worker: WorkerSetup, unserialize_report) -> None: + worker.pytester.makepyfile( """ def test_func(): pass def test_func2(): pass @@ -183,17 +183,19 @@ def test_func2(): pass ev = worker.popevent("workerfinished") assert "workeroutput" in ev.kwargs - def test_happy_run_events_converted(self, testdir, worker): - py.test.xfail("implement a simple test for event production") - assert not worker.use_callback - worker.testdir.makepyfile( + def test_happy_run_events_converted( + self, pytester: pytest.Pytester, worker: WorkerSetup + ) -> None: + pytest.xfail("implement a simple test for event production") + assert not worker.use_callback # type: ignore[unreachable] + worker.pytester.makepyfile( """ def test_func(): pass """ ) worker.setup() - hookrec = testdir.getreportrecorder(worker.config) + hookrec = pytester.getreportrecorder(worker.config) for data in worker.slp.channel: worker.slp.process_from_remote(data) worker.slp.process_from_remote(worker.slp.ENDMARK) @@ -209,7 +211,9 @@ def test_func(): ] ) - def test_process_from_remote_error_handling(self, worker, capsys): + def test_process_from_remote_error_handling( + self, worker: WorkerSetup, capsys: pytest.CaptureFixture[str] + ) -> None: worker.use_callback = True worker.setup() worker.slp.process_from_remote(("", ())) @@ -219,8 +223,8 @@ def test_process_from_remote_error_handling(self, worker, capsys): assert ev.name == "errordown" -def test_remote_env_vars(testdir): - testdir.makepyfile( +def test_remote_env_vars(pytester: pytest.Pytester) -> None: + pytester.makepyfile( """ import os def test(): @@ -229,13 +233,13 @@ def test(): assert os.environ['PYTEST_XDIST_WORKER_COUNT'] == '2' """ ) - result = testdir.runpytest("-n2", "--max-worker-restart=0") + result = pytester.runpytest("-n2", "--max-worker-restart=0") assert result.ret == 0 -def test_remote_inner_argv(testdir): +def test_remote_inner_argv(pytester: pytest.Pytester) -> None: """Test/document the behavior due to execnet using `python -c`.""" - testdir.makepyfile( + pytester.makepyfile( """ import sys @@ -243,14 +247,14 @@ def test_argv(): assert sys.argv == ["-c"] """ ) - result = testdir.runpytest("-n1") + result = pytester.runpytest("-n1") assert result.ret == 0 -def test_remote_mainargv(testdir): +def test_remote_mainargv(pytester: pytest.Pytester) -> None: outer_argv = sys.argv - testdir.makepyfile( + pytester.makepyfile( """ def test_mainargv(request): assert request.config.workerinput["mainargv"] == {!r} @@ -258,14 +262,14 @@ def test_mainargv(request): outer_argv ) ) - result = testdir.runpytest("-n1") + result = pytester.runpytest("-n1") assert result.ret == 0 -def test_remote_usage_prog(testdir, request): +def test_remote_usage_prog(pytester: pytest.Pytester, request) -> None: if not hasattr(request.config._parser, "prog"): pytest.skip("prog not available in config parser") - testdir.makeconftest( + pytester.makeconftest( """ import pytest @@ -280,7 +284,7 @@ def pytest_configure(config): config_parser = config._parser """ ) - testdir.makepyfile( + pytester.makepyfile( """ import sys @@ -289,14 +293,14 @@ def test(get_config_parser, request): """ ) - result = testdir.runpytest_subprocess("-n1") + result = pytester.runpytest_subprocess("-n1") assert result.ret == 1 result.stdout.fnmatch_lines(["*usage: *", "*error: my_usage_error"]) -def test_remote_sys_path(testdir): +def test_remote_sys_path(pytester: pytest.Pytester) -> None: """Work around sys.path differences due to execnet using `python -c`.""" - testdir.makepyfile( + pytester.makepyfile( """ import sys @@ -304,5 +308,5 @@ def test_sys_path(): assert "" not in sys.path """ ) - result = testdir.runpytest("-n1") + result = pytester.runpytest("-n1") assert result.ret == 0 diff --git a/testing/test_workermanage.py b/testing/test_workermanage.py index 3cf19a8f..fae06011 100644 --- a/testing/test_workermanage.py +++ b/testing/test_workermanage.py @@ -1,39 +1,42 @@ +import execnet import py import pytest +import shutil import textwrap -import execnet -from _pytest.pytester import HookRecorder -from xdist import workermanage, newhooks +from pathlib import Path +from xdist import workermanage from xdist.workermanage import HostRSync, NodeManager pytest_plugins = "pytester" @pytest.fixture -def hookrecorder(request, config): - hookrecorder = HookRecorder(config.pluginmanager) - if hasattr(hookrecorder, "start_recording"): - hookrecorder.start_recording(newhooks) - request.addfinalizer(hookrecorder.finish_recording) +def hookrecorder(request, config, pytester: pytest.Pytester): + hookrecorder = pytester.make_hook_recorder(config.pluginmanager) return hookrecorder @pytest.fixture -def config(testdir): - return testdir.parseconfig() +def config(pytester: pytest.Pytester): + return pytester.parseconfig() @pytest.fixture -def mysetup(tmpdir): - class mysetup: - source = tmpdir.mkdir("source") - dest = tmpdir.mkdir("dest") +def source(tmp_path: Path) -> Path: + source = tmp_path / "source" + source.mkdir() + return source - return mysetup() + +@pytest.fixture +def dest(tmp_path: Path) -> Path: + dest = tmp_path / "dest" + dest.mkdir() + return dest @pytest.fixture -def workercontroller(monkeypatch): +def workercontroller(monkeypatch: pytest.MonkeyPatch): class MockController: def __init__(self, *args): pass @@ -46,18 +49,20 @@ def setup(self): class TestNodeManagerPopen: - def test_popen_no_default_chdir(self, config): + def test_popen_no_default_chdir(self, config) -> None: gm = NodeManager(config, ["popen"]) assert gm.specs[0].chdir is None - def test_default_chdir(self, config): + def test_default_chdir(self, config) -> None: specs = ["ssh=noco", "socket=xyz"] for spec in NodeManager(config, specs).specs: assert spec.chdir == "pyexecnetcache" for spec in NodeManager(config, specs, defaultchdir="abc").specs: assert spec.chdir == "abc" - def test_popen_makegateway_events(self, config, hookrecorder, workercontroller): + def test_popen_makegateway_events( + self, config, hookrecorder, workercontroller + ) -> None: hm = NodeManager(config, ["popen"] * 2) hm.setup_nodes(None) call = hookrecorder.popcall("pytest_xdist_setupnodes") @@ -72,15 +77,16 @@ def test_popen_makegateway_events(self, config, hookrecorder, workercontroller): hm.teardown_nodes() assert not len(hm.group) - def test_popens_rsync(self, config, mysetup, workercontroller): - source = mysetup.source + def test_popens_rsync( + self, config, source: Path, dest: Path, workercontroller + ) -> None: hm = NodeManager(config, ["popen"] * 2) hm.setup_nodes(None) assert len(hm.group) == 2 for gw in hm.group: class pseudoexec: - args = [] + args = [] # type: ignore[var-annotated] def __init__(self, *args): self.args.extend(args) @@ -97,30 +103,37 @@ def waitclose(self): assert not len(hm.group) assert "sys.path.insert" in gw.remote_exec.args[0] - def test_rsync_popen_with_path(self, config, mysetup, workercontroller): - source, dest = mysetup.source, mysetup.dest + def test_rsync_popen_with_path( + self, config, source: Path, dest: Path, workercontroller + ) -> None: hm = NodeManager(config, ["popen//chdir=%s" % dest] * 1) hm.setup_nodes(None) - source.ensure("dir1", "dir2", "hello") + source.joinpath("dir1", "dir2").mkdir(parents=True) + source.joinpath("dir1", "dir2", "hello").touch() notifications = [] for gw in hm.group: hm.rsync(gw, source, notify=lambda *args: notifications.append(args)) assert len(notifications) == 1 assert notifications[0] == ("rsyncrootready", hm.group["gw0"].spec, source) hm.teardown_nodes() - dest = dest.join(source.basename) - assert dest.join("dir1").check() - assert dest.join("dir1", "dir2").check() - assert dest.join("dir1", "dir2", "hello").check() + dest = dest.joinpath(source.name) + assert dest.joinpath("dir1").exists() + assert dest.joinpath("dir1", "dir2").exists() + assert dest.joinpath("dir1", "dir2", "hello").exists() def test_rsync_same_popen_twice( - self, config, mysetup, hookrecorder, workercontroller - ): - source, dest = mysetup.source, mysetup.dest + self, + config, + source: Path, + dest: Path, + hookrecorder, + workercontroller, + ) -> None: hm = NodeManager(config, ["popen//chdir=%s" % dest] * 2) hm.roots = [] hm.setup_nodes(None) - source.ensure("dir1", "dir2", "hello") + source.joinpath("dir1", "dir2").mkdir(parents=True) + source.joinpath("dir1", "dir2", "hello").touch() gw = hm.group[0] hm.rsync(gw, source) call = hookrecorder.popcall("pytest_xdist_rsyncstart") @@ -131,83 +144,98 @@ def test_rsync_same_popen_twice( class TestHRSync: - def test_hrsync_filter(self, mysetup): - source, _ = mysetup.source, mysetup.dest # noqa - source.ensure("dir", "file.txt") - source.ensure(".svn", "entries") - source.ensure(".somedotfile", "moreentries") - source.ensure("somedir", "editfile~") + def test_hrsync_filter(self, source: Path, dest: Path) -> None: + source.joinpath("dir").mkdir() + source.joinpath("dir", "file.txt").touch() + source.joinpath(".svn").mkdir() + source.joinpath(".svn", "entries").touch() + source.joinpath(".somedotfile").mkdir() + source.joinpath(".somedotfile", "moreentries").touch() + source.joinpath("somedir").mkdir() + source.joinpath("somedir", "editfile~").touch() syncer = HostRSync(source, ignores=NodeManager.DEFAULT_IGNORES) - files = list(source.visit(rec=syncer.filter, fil=syncer.filter)) + files = list(py.path.local(source).visit(rec=syncer.filter, fil=syncer.filter)) assert len(files) == 3 basenames = [x.basename for x in files] assert "dir" in basenames assert "file.txt" in basenames assert "somedir" in basenames - def test_hrsync_one_host(self, mysetup): - source, dest = mysetup.source, mysetup.dest + def test_hrsync_one_host(self, source: Path, dest: Path) -> None: gw = execnet.makegateway("popen//chdir=%s" % dest) finished = [] rsync = HostRSync(source) rsync.add_target_host(gw, finished=lambda: finished.append(1)) - source.join("hello.py").write("world") + source.joinpath("hello.py").write_text("world") rsync.send() gw.exit() - assert dest.join(source.basename, "hello.py").check() + assert dest.joinpath(source.name, "hello.py").exists() assert len(finished) == 1 class TestNodeManager: - @py.test.mark.xfail(run=False) - def test_rsync_roots_no_roots(self, testdir, mysetup): - mysetup.source.ensure("dir1", "file1").write("hello") - config = testdir.parseconfig(mysetup.source) - nodemanager = NodeManager(config, ["popen//chdir=%s" % mysetup.dest]) + @pytest.mark.xfail(run=False) + def test_rsync_roots_no_roots( + self, pytester: pytest.Pytester, source: Path, dest: Path + ) -> None: + source.joinpath("dir1").mkdir() + source.joinpath("dir1", "file1").write_text("hello") + config = pytester.parseconfig(source) + nodemanager = NodeManager(config, ["popen//chdir=%s" % dest]) # assert nodemanager.config.topdir == source == config.topdir - nodemanager.makegateways() - nodemanager.rsync_roots() - (p,) = nodemanager.gwmanager.multi_exec( + nodemanager.makegateways() # type: ignore[attr-defined] + nodemanager.rsync_roots() # type: ignore[call-arg] + (p,) = nodemanager.gwmanager.multi_exec( # type: ignore[attr-defined] "import os ; channel.send(os.getcwd())" ).receive_each() - p = py.path.local(p) + p = Path(p) print("remote curdir", p) - assert p == mysetup.dest.join(config.topdir.basename) - assert p.join("dir1").check() - assert p.join("dir1", "file1").check() - - def test_popen_rsync_subdir(self, testdir, mysetup, workercontroller): - source, dest = mysetup.source, mysetup.dest - dir1 = mysetup.source.mkdir("dir1") - dir2 = dir1.mkdir("dir2") - dir2.ensure("hello") + assert p == dest.joinpath(config.rootpath.name) + assert p.joinpath("dir1").check() + assert p.joinpath("dir1", "file1").check() + + def test_popen_rsync_subdir( + self, pytester: pytest.Pytester, source: Path, dest: Path, workercontroller + ) -> None: + dir1 = source / "dir1" + dir1.mkdir() + dir2 = dir1 / "dir2" + dir2.mkdir() + dir2.joinpath("hello").touch() for rsyncroot in (dir1, source): - dest.remove() + shutil.rmtree(str(dest), ignore_errors=True) nodemanager = NodeManager( - testdir.parseconfig( + pytester.parseconfig( "--tx", "popen//chdir=%s" % dest, "--rsyncdir", rsyncroot, source ) ) nodemanager.setup_nodes(None) # calls .rsync_roots() if rsyncroot == source: - dest = dest.join("source") - assert dest.join("dir1").check() - assert dest.join("dir1", "dir2").check() - assert dest.join("dir1", "dir2", "hello").check() + dest = dest.joinpath("source") + assert dest.joinpath("dir1").exists() + assert dest.joinpath("dir1", "dir2").exists() + assert dest.joinpath("dir1", "dir2", "hello").exists() nodemanager.teardown_nodes() @pytest.mark.parametrize( "flag, expects_report", [("-q", False), ("", False), ("-v", True)] ) def test_rsync_report( - self, testdir, mysetup, workercontroller, capsys, flag, expects_report - ): - source, dest = mysetup.source, mysetup.dest - dir1 = mysetup.source.mkdir("dir1") - args = "--tx", "popen//chdir=%s" % dest, "--rsyncdir", dir1, source + self, + pytester: pytest.Pytester, + source: Path, + dest: Path, + workercontroller, + capsys: pytest.CaptureFixture[str], + flag: str, + expects_report: bool, + ) -> None: + dir1 = source / "dir1" + dir1.mkdir() + args = ["--tx", "popen//chdir=%s" % dest, "--rsyncdir", str(dir1), str(source)] if flag: - args += (flag,) - nodemanager = NodeManager(testdir.parseconfig(*args)) + args.append(flag) + nodemanager = NodeManager(pytester.parseconfig(*args)) nodemanager.setup_nodes(None) # calls .rsync_roots() out, _ = capsys.readouterr() if expects_report: @@ -215,77 +243,86 @@ def test_rsync_report( else: assert "<= pytest/__init__.py" not in out - def test_init_rsync_roots(self, testdir, mysetup, workercontroller): - source, dest = mysetup.source, mysetup.dest - dir2 = source.ensure("dir1", "dir2", dir=1) - source.ensure("dir1", "somefile", dir=1) - dir2.ensure("hello") - source.ensure("bogusdir", "file") - source.join("tox.ini").write( + def test_init_rsync_roots( + self, pytester: pytest.Pytester, source: Path, dest: Path, workercontroller + ) -> None: + dir2 = source.joinpath("dir1", "dir2") + dir2.mkdir(parents=True) + source.joinpath("dir1", "somefile").mkdir() + dir2.joinpath("hello").touch() + source.joinpath("bogusdir").mkdir() + source.joinpath("bogusdir", "file").touch() + source.joinpath("tox.ini").write_text( textwrap.dedent( """ - [pytest] - rsyncdirs=dir1/dir2 - """ + [pytest] + rsyncdirs=dir1/dir2 + """ ) ) - config = testdir.parseconfig(source) + config = pytester.parseconfig(source) nodemanager = NodeManager(config, ["popen//chdir=%s" % dest]) nodemanager.setup_nodes(None) # calls .rsync_roots() - assert dest.join("dir2").check() - assert not dest.join("dir1").check() - assert not dest.join("bogus").check() - - def test_rsyncignore(self, testdir, mysetup, workercontroller): - source, dest = mysetup.source, mysetup.dest - dir2 = source.ensure("dir1", "dir2", dir=1) - source.ensure("dir5", "dir6", "bogus") - source.ensure("dir5", "file") - dir2.ensure("hello") - source.ensure("foo", "bar") - source.ensure("bar", "foo") - source.join("tox.ini").write( + assert dest.joinpath("dir2").exists() + assert not dest.joinpath("dir1").exists() + assert not dest.joinpath("bogus").exists() + + def test_rsyncignore( + self, pytester: pytest.Pytester, source: Path, dest: Path, workercontroller + ) -> None: + dir2 = source.joinpath("dir1", "dir2") + dir2.mkdir(parents=True) + source.joinpath("dir5", "dir6").mkdir(parents=True) + source.joinpath("dir5", "dir6", "bogus").touch() + source.joinpath("dir5", "file").touch() + dir2.joinpath("hello").touch() + source.joinpath("foo").mkdir() + source.joinpath("foo", "bar").touch() + source.joinpath("bar").mkdir() + source.joinpath("bar", "foo").touch() + source.joinpath("tox.ini").write_text( textwrap.dedent( """ - [pytest] - rsyncdirs = dir1 dir5 - rsyncignore = dir1/dir2 dir5/dir6 foo* - """ + [pytest] + rsyncdirs = dir1 dir5 + rsyncignore = dir1/dir2 dir5/dir6 foo* + """ ) ) - config = testdir.parseconfig(source) + config = pytester.parseconfig(source) config.option.rsyncignore = ["bar"] nodemanager = NodeManager(config, ["popen//chdir=%s" % dest]) nodemanager.setup_nodes(None) # calls .rsync_roots() - assert dest.join("dir1").check() - assert not dest.join("dir1", "dir2").check() - assert dest.join("dir5", "file").check() - assert not dest.join("dir6").check() - assert not dest.join("foo").check() - assert not dest.join("bar").check() - - def test_optimise_popen(self, testdir, mysetup, workercontroller): - source = mysetup.source + assert dest.joinpath("dir1").exists() + assert not dest.joinpath("dir1", "dir2").exists() + assert dest.joinpath("dir5", "file").exists() + assert not dest.joinpath("dir6").exists() + assert not dest.joinpath("foo").exists() + assert not dest.joinpath("bar").exists() + + def test_optimise_popen( + self, pytester: pytest.Pytester, source: Path, dest: Path, workercontroller + ) -> None: specs = ["popen"] * 3 - source.join("conftest.py").write("rsyncdirs = ['a']") - source.ensure("a", dir=1) - config = testdir.parseconfig(source) + source.joinpath("conftest.py").write_text("rsyncdirs = ['a']") + source.joinpath("a").mkdir() + config = pytester.parseconfig(source) nodemanager = NodeManager(config, specs) nodemanager.setup_nodes(None) # calls .rysnc_roots() for gwspec in nodemanager.specs: assert gwspec._samefilesystem() assert not gwspec.chdir - def test_ssh_setup_nodes(self, specssh, testdir): - testdir.makepyfile( + def test_ssh_setup_nodes(self, specssh: str, pytester: pytest.Pytester) -> None: + pytester.makepyfile( __init__="", test_x=""" def test_one(): pass """, ) - reprec = testdir.inline_run( - "-d", "--rsyncdir=%s" % testdir.tmpdir, "--tx", specssh, testdir.tmpdir + reprec = pytester.inline_run( + "-d", "--rsyncdir=%s" % pytester.path, "--tx", specssh, pytester.path ) (rep,) = reprec.getreports("pytest_runtest_logreport") assert rep.passed diff --git a/tox.ini b/tox.ini index 9d635039..3ab7109d 100644 --- a/tox.ini +++ b/tox.ini @@ -18,7 +18,6 @@ commands= extras = testing psutil -deps = pytest commands = pytest {posargs:-k psutil} From 787b8d9c14ca663d95632e43d55adcc4c76058c7 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 30 Oct 2021 12:18:11 +0300 Subject: [PATCH 063/113] Add basic mypy linting Based on pytest setup, though more lax. Currently only the tests are annotated so only helpful for them. Fix #721. --- .pre-commit-config.yaml | 9 +++++++++ changelog/721.trivial.rst | 1 + setup.cfg | 16 ++++++++++++++++ src/xdist/plugin.py | 2 +- src/xdist/remote.py | 10 +++++----- 5 files changed, 32 insertions(+), 6 deletions(-) create mode 100644 changelog/721.trivial.rst diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 43a05781..b1dda8aa 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -28,3 +28,12 @@ repos: files: ^(CHANGELOG.rst|HOWTORELEASE.rst|README.rst|changelog/.*)$ language: python additional_dependencies: [pygments, restructuredtext_lint] +- repo: https://github.com/pre-commit/mirrors-mypy + rev: v0.910-1 + hooks: + - id: mypy + files: ^(src/|testing/) + args: [] + additional_dependencies: + - pytest>=6.2.0 + - py>=1.10.0 diff --git a/changelog/721.trivial.rst b/changelog/721.trivial.rst new file mode 100644 index 00000000..1c32376c --- /dev/null +++ b/changelog/721.trivial.rst @@ -0,0 +1 @@ +Started using type annotations and mypy checking internally. The types are incomplete and not published. diff --git a/setup.cfg b/setup.cfg index d09d11a5..ce97d810 100644 --- a/setup.cfg +++ b/setup.cfg @@ -58,3 +58,19 @@ setproctitle = setproctitle [flake8] max-line-length = 100 + +[mypy] +mypy_path = src +# TODO: Enable this & fix errors. +# check_untyped_defs = True +disallow_any_generics = True +ignore_missing_imports = True +no_implicit_optional = True +show_error_codes = True +strict_equality = True +warn_redundant_casts = True +warn_return_any = True +warn_unreachable = True +warn_unused_configs = True +# TODO: Enable this & fix errors. +# no_implicit_reexport = True diff --git a/src/xdist/plugin.py b/src/xdist/plugin.py index 12b3a0ea..4f410cc1 100644 --- a/src/xdist/plugin.py +++ b/src/xdist/plugin.py @@ -250,7 +250,7 @@ def is_xdist_controller(request_or_session) -> bool: is_xdist_master = is_xdist_controller -def get_xdist_worker_id(request_or_session) -> str: +def get_xdist_worker_id(request_or_session): """Return the id of the current worker ('gw0', 'gw1', etc) or 'master' if running on the controller node. diff --git a/src/xdist/remote.py b/src/xdist/remote.py index 0194ae02..05951f31 100644 --- a/src/xdist/remote.py +++ b/src/xdist/remote.py @@ -236,8 +236,8 @@ def setup_config(config, basetemp): if __name__ == "__channelexec__": - channel = channel # noqa - workerinput, args, option_dict, change_sys_path = channel.receive() + channel = channel # type: ignore[name-defined] # noqa: F821 + workerinput, args, option_dict, change_sys_path = channel.receive() # type: ignore[name-defined] if change_sys_path is None: importpath = os.getcwd() @@ -260,7 +260,7 @@ def setup_config(config, basetemp): setup_config(config, option_dict.get("basetemp")) config._parser.prog = os.path.basename(workerinput["mainargv"][0]) - config.workerinput = workerinput - config.workeroutput = {} - interactor = WorkerInteractor(config, channel) + config.workerinput = workerinput # type: ignore[attr-defined] + config.workeroutput = {} # type: ignore[attr-defined] + interactor = WorkerInteractor(config, channel) # type: ignore[name-defined] config.hook.pytest_cmdline_main(config=config) From d794b27e72d4f44ea7bbd72b60b360ba2ecc9259 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 30 Oct 2021 21:27:26 +0300 Subject: [PATCH 064/113] Use config._tmp_path_factory instead of config._tmpdirhandler Both are private, and shouldn't be used, but not sure how to fix it currently. But `_tmpdirhandler` will be relegated to the legacypath plugin so should extra not be used. --- src/xdist/workermanage.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/xdist/workermanage.py b/src/xdist/workermanage.py index 7f0bce2f..4b576f93 100644 --- a/src/xdist/workermanage.py +++ b/src/xdist/workermanage.py @@ -254,9 +254,9 @@ def setup(self): args = make_reltoroot(self.nodemanager.roots, args) if spec.popen: name = "popen-%s" % self.gateway.id - if hasattr(self.config, "_tmpdirhandler"): - basetemp = self.config._tmpdirhandler.getbasetemp() - option_dict["basetemp"] = str(basetemp.join(name)) + if hasattr(self.config, "_tmp_path_factory"): + basetemp = self.config._tmp_path_factory.getbasetemp() + option_dict["basetemp"] = str(basetemp / name) self.config.hook.pytest_configure_node(node=self) remote_module = self.config.hook.pytest_xdist_getremotemodule() From ae58690f3e9bacddd4ba793fd1413f570f614793 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 30 Oct 2021 20:13:32 +0300 Subject: [PATCH 065/113] Avoid soft-deprecated pathlist ini type on pytest>=7 This allows the test suite to succeed with the legacypath plugin blocked. Fix #722. --- changelog/722.feature.rst | 1 + src/xdist/looponfail.py | 2 +- src/xdist/plugin.py | 12 ++++++++---- src/xdist/workermanage.py | 4 ++-- 4 files changed, 12 insertions(+), 7 deletions(-) create mode 100644 changelog/722.feature.rst diff --git a/changelog/722.feature.rst b/changelog/722.feature.rst new file mode 100644 index 00000000..b8654ffb --- /dev/null +++ b/changelog/722.feature.rst @@ -0,0 +1 @@ +Full compatibility with pytest 7 - no deprecation warnings or use of legacy features. diff --git a/src/xdist/looponfail.py b/src/xdist/looponfail.py index 5ce13a02..b721527a 100644 --- a/src/xdist/looponfail.py +++ b/src/xdist/looponfail.py @@ -38,7 +38,7 @@ def pytest_cmdline_main(config): def looponfail_main(config): remotecontrol = RemoteControl(config) - rootdirs = config.getini("looponfailroots") + rootdirs = [py.path.local(root) for root in config.getini("looponfailroots")] statrecorder = StatRecorder(rootdirs) try: while 1: diff --git a/src/xdist/plugin.py b/src/xdist/plugin.py index 4f410cc1..e65bb4aa 100644 --- a/src/xdist/plugin.py +++ b/src/xdist/plugin.py @@ -1,10 +1,14 @@ import os import uuid import sys +from pathlib import Path import py import pytest + +PYTEST_GTE_7 = hasattr(pytest, "version_tuple") and pytest.version_tuple >= (7, 0) # type: ignore[attr-defined] + _sys_path = list(sys.path) # freeze a copy of sys.path at interpreter startup @@ -147,18 +151,18 @@ def pytest_addoption(parser): parser.addini( "rsyncdirs", "list of (relative) paths to be rsynced for remote distributed testing.", - type="pathlist", + type="paths" if PYTEST_GTE_7 else "pathlist", ) parser.addini( "rsyncignore", "list of (relative) glob-style paths to be ignored for rsyncing.", - type="pathlist", + type="paths" if PYTEST_GTE_7 else "pathlist", ) parser.addini( "looponfailroots", - type="pathlist", + type="paths" if PYTEST_GTE_7 else "pathlist", help="directories to check for changes", - default=[py.path.local()], + default=[Path.cwd() if PYTEST_GTE_7 else py.path.local()], ) diff --git a/src/xdist/workermanage.py b/src/xdist/workermanage.py index 4b576f93..411ea824 100644 --- a/src/xdist/workermanage.py +++ b/src/xdist/workermanage.py @@ -118,8 +118,8 @@ def get_dir(p): def _getrsyncoptions(self): """Get options to be passed for rsync.""" ignores = list(self.DEFAULT_IGNORES) - ignores += self.config.option.rsyncignore - ignores += self.config.getini("rsyncignore") + ignores += [str(path) for path in self.config.option.rsyncignore] + ignores += [str(path) for path in self.config.getini("rsyncignore")] return { "ignores": ignores, From 0fac79b29cdbe174a7aeca75c8e4160f09c328d9 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Mon, 1 Nov 2021 09:24:59 +0200 Subject: [PATCH 066/113] README: fill in TBD version --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 5394aad0..1af2f6ae 100644 --- a/README.rst +++ b/README.rst @@ -320,7 +320,7 @@ Since version 2.0, the following functions are also available in the ``xdist`` m Identifying workers from the system environment ----------------------------------------------- -*New in version UNRELEASED TBD FIXME* +*New in version 2.4* If the `setproctitle`_ package is installed, ``pytest-xdist`` will use it to update the process title (command line) on its workers to show their current From 475d7c4c2a0aa087dbafe57c35d8b46c20d337c7 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Mon, 1 Nov 2021 09:36:12 +0200 Subject: [PATCH 067/113] README,OVERVIEW: replace master -> controller --- OVERVIEW.md | 38 +++++++++++++++++++------------------- README.rst | 27 ++++++++++----------------- 2 files changed, 29 insertions(+), 36 deletions(-) diff --git a/OVERVIEW.md b/OVERVIEW.md index 87b9d1b3..da0d3c4d 100644 --- a/OVERVIEW.md +++ b/OVERVIEW.md @@ -1,36 +1,36 @@ # Overview # `xdist` works by spawning one or more **workers**, which are controlled -by the **master**. Each **worker** is responsible for performing -a full test collection and afterwards running tests as dictated by the **master**. +by the **controller**. Each **worker** is responsible for performing +a full test collection and afterwards running tests as dictated by the **controller**. The execution flow is: -1. **master** spawns one or more **workers** at the beginning of - the test session. The communication between **master** and **worker** nodes makes use of - [execnet](http://codespeak.net/execnet/) and its [gateways](http://codespeak.net/execnet/basics.html#gateways-bootstrapping-python-interpreters). +1. **controller** spawns one or more **workers** at the beginning of + the test session. The communication between **controller** and **worker** nodes makes use of + [execnet](https://codespeak.net/execnet/) and its [gateways](https://codespeak.net/execnet/basics.html#gateways-bootstrapping-python-interpreters). The actual interpreters executing the code for the **workers** might be remote or local. 1. Each **worker** itself is a mini pytest runner. **workers** at this point perform a full test collection, sending back the collected - test-ids back to the **master** which does not + test-ids back to the **controller** which does not perform any collection itself. -1. The **master** receives the result of the collection from all nodes. - At this point the **master** performs some sanity check to ensure that +1. The **controller** receives the result of the collection from all nodes. + At this point the **controller** performs some sanity check to ensure that all **workers** collected the same tests (including order), bailing out otherwise. If all is well, it converts the list of test-ids into a list of simple indexes, where each index corresponds to the position of that test in the original collection list. This works because all nodes have the same - collection list, and saves bandwidth because the **master** can now tell + collection list, and saves bandwidth because the **controller** can now tell one of the workers to just *execute test index 3* index of passing the full test id. -1. If **dist-mode** is **each**: the **master** just sends the full list +1. If **dist-mode** is **each**: the **controller** just sends the full list of test indexes to each node at this moment. -1. If **dist-mode** is **load**: the **master** takes around 25% of the +1. If **dist-mode** is **load**: the **controller** takes around 25% of the tests and sends them one by one to each **worker** in a round robin fashion. The rest of the tests will be distributed later as **workers** finish tests (see below). @@ -40,36 +40,36 @@ The execution flow is: 1. **workers** re-implement `pytest_runtestloop`: pytest's default implementation basically loops over all collected items in the `session` object and executes the `pytest_runtest_protocol` for each test item, but in xdist **workers** sit idly - waiting for **master** to send tests for execution. As tests are + waiting for **controller** to send tests for execution. As tests are received by **workers**, `pytest_runtest_protocol` is executed for each test. Here it worth noting an implementation detail: **workers** always must keep at least one test item on their queue due to how the `pytest_runtest_protocol(item, nextitem)` hook is defined: in order to pass the `nextitem` to the hook, the worker must wait for more - instructions from master before executing that remaining test. If it receives more tests, + instructions from controller before executing that remaining test. If it receives more tests, then it can safely call `pytest_runtest_protocol` because it knows what the `nextitem` parameter will be. If it receives a "shutdown" signal, then it can execute the hook passing `nextitem` as `None`. 1. As tests are started and completed at the **workers**, the results are sent - back to the **master**, which then just forwards the results to + back to the **controller**, which then just forwards the results to the appropriate pytest hooks: `pytest_runtest_logstart` and `pytest_runtest_logreport`. This way other plugins (for example `junitxml`) - can work normally. The **master** (when in dist-mode **load**) + can work normally. The **controller** (when in dist-mode **load**) decides to send more tests to a node when a test completes, using some heuristics such as test durations and how many tests each **worker** still has to run. -1. When the **master** has no more pending tests it will +1. When the **controller** has no more pending tests it will send a "shutdown" signal to all **workers**, which will then run their remaining tests to completion and shut down. At this point the - **master** will sit waiting for **workers** to shut down, still + **controller** will sit waiting for **workers** to shut down, still processing events such as `pytest_runtest_logreport`. ## FAQ ## > Why does each worker do its own collection, as opposed to having -the master collect once and distribute from that collection to the workers? +the controller collect once and distribute from that collection to the workers? -If collection was performed by master then it would have to +If collection was performed by controller then it would have to serialize collected items to send them through the wire, as workers live in another process. The problem is that test items are not easily (impossible?) to serialize, as they contain references to the test functions, fixture managers, config objects, etc. Even if one manages to serialize it, diff --git a/README.rst b/README.rst index 5394aad0..53849fba 100644 --- a/README.rst +++ b/README.rst @@ -243,11 +243,11 @@ environment this command will send each tests to all platforms - and report back failures from all platforms at once. The specifications strings use the `xspec syntax`_. -.. _`xspec syntax`: http://codespeak.net/execnet/basics.html#xspec +.. _`xspec syntax`: https://codespeak.net/execnet/basics.html#xspec .. _`socketserver.py`: https://raw.githubusercontent.com/pytest-dev/execnet/master/execnet/script/socketserver.py -.. _`execnet`: http://codespeak.net/execnet +.. _`execnet`: https://codespeak.net/execnet Identifying the worker process during a test -------------------------------------------- @@ -287,17 +287,6 @@ Since version 2.0, the following functions are also available in the ``xdist`` m :param request_or_session: the `pytest` `request` or `session` object """ - def is_xdist_master(request_or_session) -> bool: - """Return `True` if this is the xdist controller, `False` otherwise - - Note: this method also returns `False` when distribution has not been - activated at all. - - deprecated alias for is_xdist_controller - - :param request_or_session: the `pytest` `request` or `session` object - """ - def is_xdist_controller(request_or_session) -> bool: """Return `True` if this is the xdist controller, `False` otherwise @@ -307,11 +296,15 @@ Since version 2.0, the following functions are also available in the ``xdist`` m :param request_or_session: the `pytest` `request` or `session` object """ + def is_xdist_master(request_or_session) -> bool: + """Deprecated alias for is_xdist_controller.""" + def get_xdist_worker_id(request_or_session) -> str: """Return the id of the current worker ('gw0', 'gw1', etc) or 'master' if running on the controller node. - If not distributing tests (for example passing `-n0` or not passing `-n` at all) also return 'master'. + If not distributing tests (for example passing `-n0` or not passing `-n` at all) + also return 'master'. :param request_or_session: the `pytest` `request` or `session` object """ @@ -371,10 +364,10 @@ Additionally, during a test run, the following environment variable is defined: * ``PYTEST_XDIST_TESTRUNUID``: the unique id of the test run. -Accessing ``sys.argv`` from the master node in workers ------------------------------------------------------- +Accessing ``sys.argv`` from the controller node in workers +---------------------------------------------------------- -To access the ``sys.argv`` passed to the command-line of the master node, use +To access the ``sys.argv`` passed to the command-line of the controller node, use ``request.config.workerinput["mainargv"]``. From bf9606c6728d7d9e8ad5d87afda7ce76c706c9ab Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 1 Nov 2021 20:27:36 +0000 Subject: [PATCH 068/113] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/psf/black: 21.9b0 → 21.10b0](https://github.com/psf/black/compare/21.9b0...21.10b0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b1dda8aa..598f545f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/psf/black - rev: 21.9b0 + rev: 21.10b0 hooks: - id: black args: [--safe, --quiet, --target-version, py35] From 89a3df3e2075a014afeed56a3e84871394956294 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Tue, 2 Nov 2021 09:00:24 +0200 Subject: [PATCH 069/113] Deprecate --boxed This will allow removing the pytest-forked dependency in the future. Refs #468. --- changelog/468.deprecation.rst | 3 +++ example/boxed.txt | 6 +++--- src/xdist/plugin.py | 6 ++++++ testing/acceptance_test.py | 2 +- 4 files changed, 13 insertions(+), 4 deletions(-) create mode 100644 changelog/468.deprecation.rst diff --git a/changelog/468.deprecation.rst b/changelog/468.deprecation.rst new file mode 100644 index 00000000..e3109c9e --- /dev/null +++ b/changelog/468.deprecation.rst @@ -0,0 +1,3 @@ +The ``--boxed`` commmand line argument is deprecated. +Install pytest-forked and use ``--forked`` instead. +pytest-xdist 3.0.0 will remove the ``--boxed`` argument and pytest-forked dependency. diff --git a/example/boxed.txt b/example/boxed.txt index aabb27e3..81543ab3 100644 --- a/example/boxed.txt +++ b/example/boxed.txt @@ -1,9 +1,9 @@ -.. note:: +.. warning:: Since 1.19.0, the actual implementation of the ``--boxed`` option has been moved to a separate plugin, `pytest-forked `_ - which can be installed independently. The ``--boxed`` command-line options remains - for backward compatibility reasons. + which can be installed independently. The ``--boxed`` command-line option is deprecated + and will be removed in pytest-xdist 3.0.0; use ``--forked`` from pytest-forked instead. If your testing involves C or C++ libraries you might have to deal diff --git a/src/xdist/plugin.py b/src/xdist/plugin.py index e65bb4aa..5f1ed099 100644 --- a/src/xdist/plugin.py +++ b/src/xdist/plugin.py @@ -193,6 +193,12 @@ def pytest_configure(config): if tr: tr.showfspath = False if config.getoption("boxed"): + warning = DeprecationWarning( + "The --boxed commmand line argument is deprecated. " + "Install pytest-forked and use --forked instead. " + "pytest-xdist 3.0.0 will remove the --boxed argument and pytest-forked dependency." + ) + config.issue_config_time_warning(warning, 2) config.option.forked = True diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index 3280aa18..b7dadf8d 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -738,7 +738,7 @@ def test_ok(): result.stdout.fnmatch_lines("*1 passed*") -@pytest.mark.parametrize("plugin", ["xdist.looponfail", "xdist.boxed"]) +@pytest.mark.parametrize("plugin", ["xdist.looponfail"]) def test_sub_plugins_disabled(pytester, plugin) -> None: """Test that xdist doesn't break if we disable any of its sub-plugins. (#32)""" p1 = pytester.makepyfile( From b85b71cd2212e4c2fc256c3466e08cb11ed838e7 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Tue, 2 Nov 2021 09:23:06 +0200 Subject: [PATCH 070/113] Add explicit pytest.hookspec/hookimpl annotations, avoid legacy tryfirst/trylast marks --- src/xdist/dsession.py | 12 ++++++++++-- src/xdist/looponfail.py | 5 +++++ src/xdist/newhooks.py | 8 ++++++++ src/xdist/plugin.py | 7 +++++-- src/xdist/remote.py | 10 ++++++++++ src/xdist/workermanage.py | 2 +- testing/acceptance_test.py | 2 +- 7 files changed, 40 insertions(+), 6 deletions(-) diff --git a/src/xdist/dsession.py b/src/xdist/dsession.py index d517a8a0..12539a00 100644 --- a/src/xdist/dsession.py +++ b/src/xdist/dsession.py @@ -67,7 +67,7 @@ def report_line(self, line): if self.terminal and self.config.option.verbose >= 0: self.terminal.write_line(line) - @pytest.mark.trylast + @pytest.hookimpl(trylast=True) def pytest_sessionstart(self, session): """Creates and starts the nodes. @@ -79,6 +79,7 @@ def pytest_sessionstart(self, session): self._active_nodes.update(nodes) self._session = session + @pytest.hookimpl def pytest_sessionfinish(self, session): """Shutdown all nodes.""" nm = getattr(self, "nodemanager", None) # if not fully initialized @@ -86,11 +87,12 @@ def pytest_sessionfinish(self, session): nm.teardown_nodes() self._session = None + @pytest.hookimpl def pytest_collection(self): # prohibit collection of test items in controller process return True - @pytest.mark.trylast + @pytest.hookimpl(trylast=True) def pytest_xdist_make_scheduler(self, config, log): dist = config.getvalue("dist") schedulers = { @@ -101,6 +103,7 @@ def pytest_xdist_make_scheduler(self, config, log): } return schedulers[dist](config, log) + @pytest.hookimpl def pytest_runtestloop(self): self.sched = self.config.hook.pytest_xdist_make_scheduler( config=self.config, log=self.log @@ -223,6 +226,7 @@ def worker_errordown(self, node, error): self._clone_node(node) self._active_nodes.remove(node) + @pytest.hookimpl def pytest_terminal_summary(self, terminalreporter): if self.config.option.verbose >= 0 and self._summary_report: terminalreporter.write_sep("=", "xdist: {}".format(self._summary_report)) @@ -390,6 +394,7 @@ def rewrite(self, line, newline=False): self._lastlen = len(line) self.tr.rewrite(pline, bold=True) + @pytest.hookimpl def pytest_xdist_setupnodes(self, specs): self._specs = specs for spec in specs: @@ -397,6 +402,7 @@ def pytest_xdist_setupnodes(self, specs): self.setstatus(spec, "I", show=True) self.ensure_show_status() + @pytest.hookimpl def pytest_xdist_newgateway(self, gateway): if self.config.option.verbose > 0: rinfo = gateway._rinfo() @@ -408,6 +414,7 @@ def pytest_xdist_newgateway(self, gateway): ) self.setstatus(gateway.spec, "C") + @pytest.hookimpl def pytest_testnodeready(self, node): if self.config.option.verbose > 0: d = node.workerinfo @@ -417,6 +424,7 @@ def pytest_testnodeready(self, node): self.rewrite(infoline, newline=True) self.setstatus(node.gateway.spec, "ok") + @pytest.hookimpl def pytest_testnodedown(self, node, error): if not error: return diff --git a/src/xdist/looponfail.py b/src/xdist/looponfail.py index b721527a..ef4c34ff 100644 --- a/src/xdist/looponfail.py +++ b/src/xdist/looponfail.py @@ -13,6 +13,7 @@ import execnet +@pytest.hookimpl def pytest_addoption(parser): group = parser.getgroup("xdist", "distributed and subprocess testing") group._addoption( @@ -26,6 +27,7 @@ def pytest_addoption(parser): ) +@pytest.hookimpl def pytest_cmdline_main(config): if config.getoption("looponfail"): @@ -178,6 +180,7 @@ def DEBUG(self, *args): if self.config.option.debug: print(" ".join(map(str, args))) + @pytest.hookimpl def pytest_collection(self, session): self.session = session self.trails = self.current_command @@ -192,10 +195,12 @@ def pytest_collection(self, session): hook.pytest_collection_finish(session=session) return True + @pytest.hookimpl def pytest_runtest_logreport(self, report): if report.failed: self.recorded_failures.append(report) + @pytest.hookimpl def pytest_collectreport(self, report): if report.failed: self.recorded_failures.append(report) diff --git a/src/xdist/newhooks.py b/src/xdist/newhooks.py index f9ac6b4d..77766244 100644 --- a/src/xdist/newhooks.py +++ b/src/xdist/newhooks.py @@ -14,18 +14,22 @@ import pytest +@pytest.hookspec() def pytest_xdist_setupnodes(config, specs): """called before any remote node is set up.""" +@pytest.hookspec() def pytest_xdist_newgateway(gateway): """called on new raw gateway creation.""" +@pytest.hookspec() def pytest_xdist_rsyncstart(source, gateways): """called before rsyncing a directory to remote gateways takes place.""" +@pytest.hookspec() def pytest_xdist_rsyncfinish(source, gateways): """called after rsyncing a directory to remote gateways takes place.""" @@ -35,18 +39,22 @@ def pytest_xdist_getremotemodule(): """called when creating remote node""" +@pytest.hookspec() def pytest_configure_node(node): """configure node information before it gets instantiated.""" +@pytest.hookspec() def pytest_testnodeready(node): """Test Node is ready to operate.""" +@pytest.hookspec() def pytest_testnodedown(node, error): """Test Node is down.""" +@pytest.hookspec() def pytest_xdist_node_collection_finished(node, ids): """called by the controller node when a worker node finishes collecting.""" diff --git a/src/xdist/plugin.py b/src/xdist/plugin.py index e65bb4aa..95985e1c 100644 --- a/src/xdist/plugin.py +++ b/src/xdist/plugin.py @@ -12,6 +12,7 @@ _sys_path = list(sys.path) # freeze a copy of sys.path at interpreter startup +@pytest.hookimpl def pytest_xdist_auto_num_workers(config): try: import psutil @@ -50,6 +51,7 @@ def parse_numprocesses(s): return int(s) +@pytest.hookimpl def pytest_addoption(parser): group = parser.getgroup("xdist", "distributed and subprocess testing") group._addoption( @@ -171,6 +173,7 @@ def pytest_addoption(parser): # ------------------------------------------------------------------------- +@pytest.hookimpl def pytest_addhooks(pluginmanager): from xdist import newhooks @@ -182,7 +185,7 @@ def pytest_addhooks(pluginmanager): # ------------------------------------------------------------------------- -@pytest.mark.trylast +@pytest.hookimpl(trylast=True) def pytest_configure(config): if config.getoption("dist") != "no" and not config.getvalue("collectonly"): from xdist.dsession import DSession @@ -196,7 +199,7 @@ def pytest_configure(config): config.option.forked = True -@pytest.mark.tryfirst +@pytest.hookimpl(tryfirst=True) def pytest_cmdline_main(config): usepdb = config.getoption("usepdb", False) # a core option if config.option.numprocesses in ("auto", "logical"): diff --git a/src/xdist/remote.py b/src/xdist/remote.py index 05951f31..914040d4 100644 --- a/src/xdist/remote.py +++ b/src/xdist/remote.py @@ -47,12 +47,14 @@ def sendevent(self, name, **kwargs): self.log("sending", name, kwargs) self.channel.send((name, kwargs)) + @pytest.hookimpl def pytest_internalerror(self, excrepr): formatted_error = str(excrepr) for line in formatted_error.split("\n"): self.log("IERROR>", line) interactor.sendevent("internal_error", formatted_error=formatted_error) + @pytest.hookimpl def pytest_sessionstart(self, session): self.session = session workerinfo = getinfodict() @@ -65,9 +67,11 @@ def pytest_sessionfinish(self, exitstatus): yield self.sendevent("workerfinished", workeroutput=self.config.workeroutput) + @pytest.hookimpl def pytest_collection(self, session): self.sendevent("collectionstart") + @pytest.hookimpl def pytest_runtestloop(self, session): self.log("entering main loop") torun = [] @@ -112,6 +116,7 @@ def run_one_test(self, torun): "runtest_protocol_complete", item_index=self.item_index, duration=duration ) + @pytest.hookimpl def pytest_collection_finish(self, session): try: topdir = str(self.config.rootpath) @@ -124,12 +129,15 @@ def pytest_collection_finish(self, session): ids=[item.nodeid for item in session.items], ) + @pytest.hookimpl def pytest_runtest_logstart(self, nodeid, location): self.sendevent("logstart", nodeid=nodeid, location=location) + @pytest.hookimpl def pytest_runtest_logfinish(self, nodeid, location): self.sendevent("logfinish", nodeid=nodeid, location=location) + @pytest.hookimpl def pytest_runtest_logreport(self, report): data = self.config.hook.pytest_report_to_serializable( config=self.config, report=report @@ -140,6 +148,7 @@ def pytest_runtest_logreport(self, report): assert self.session.items[self.item_index].nodeid == report.nodeid self.sendevent("testreport", data=data) + @pytest.hookimpl def pytest_collectreport(self, report): # send only reports that have not passed to controller as optimization (#330) if not report.passed: @@ -148,6 +157,7 @@ def pytest_collectreport(self, report): ) self.sendevent("collectreport", data=data) + @pytest.hookimpl def pytest_warning_recorded(self, warning_message, when, nodeid, location): self.sendevent( "warning_recorded", diff --git a/src/xdist/workermanage.py b/src/xdist/workermanage.py index 411ea824..8d291d46 100644 --- a/src/xdist/workermanage.py +++ b/src/xdist/workermanage.py @@ -212,7 +212,7 @@ class WorkerController: ENDMARK = -1 class RemoteHook: - @pytest.mark.trylast + @pytest.hookimpl(trylast=True) def pytest_xdist_getremotemodule(self): return xdist.remote diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index 3280aa18..c7dff09d 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -205,7 +205,7 @@ def test_distribution_rsyncdirs_example( def pytest_addoption(parser): parser.addoption("--foobar", action="store", dest="foobar_opt") - @pytest.mark.tryfirst + @pytest.hookimpl(tryfirst=True) def pytest_load_initial_conftests(early_config): opt = early_config.known_args_namespace.foobar_opt print("--foobar=%s active! [%s]" % (opt, os.getpid()), file=sys.stderr) From 982de538c04bfafcc1e6986c4886c0ac61704cbe Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 22 Nov 2021 20:53:14 +0000 Subject: [PATCH 071/113] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/psf/black: 21.10b0 → 21.11b1](https://github.com/psf/black/compare/21.10b0...21.11b1) - [github.com/asottile/pyupgrade: v2.29.0 → v2.29.1](https://github.com/asottile/pyupgrade/compare/v2.29.0...v2.29.1) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 598f545f..7a42be96 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/psf/black - rev: 21.10b0 + rev: 21.11b1 hooks: - id: black args: [--safe, --quiet, --target-version, py35] @@ -16,7 +16,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/asottile/pyupgrade - rev: v2.29.0 + rev: v2.29.1 hooks: - id: pyupgrade args: [--py3-plus] From 1e150204e0a3167c6d1980ce8e0cf663937f261a Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sun, 28 Nov 2021 10:30:05 -0300 Subject: [PATCH 072/113] Fix test_warning_captured_deprecated_in_pytest_6 This test started to fail in the 'py38-pytestmain' environment, the cause being PytestRemovedIn7Warning being raised by the conftest file of the test itself. Ignoring it is fine, the purpose of the test is to ensure the hook is not called by pytest-xdist. --- testing/acceptance_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index 82513a4b..31d634bf 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -790,7 +790,7 @@ def test(): warnings.warn("my custom worker warning") """ ) - result = pytester.runpytest("-n1") + result = pytester.runpytest("-n1", "-Wignore") result.stdout.fnmatch_lines(["*1 passed*"]) result.stdout.no_fnmatch_line("*this hook should not be called in this version") From 6d83034bb886f1605131e68da463070041ef67c0 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sun, 28 Nov 2021 10:30:05 -0300 Subject: [PATCH 073/113] Fix test_warning_captured_deprecated_in_pytest_6 This test started to fail in the 'py38-pytestmain' environment, the cause being PytestRemovedIn7Warning being raised by the conftest file of the test itself. Ignoring it is fine, the purpose of the test is to ensure the hook is not called by pytest-xdist. --- testing/acceptance_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index 82513a4b..31d634bf 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -790,7 +790,7 @@ def test(): warnings.warn("my custom worker warning") """ ) - result = pytester.runpytest("-n1") + result = pytester.runpytest("-n1", "-Wignore") result.stdout.fnmatch_lines(["*1 passed*"]) result.stdout.no_fnmatch_line("*this hook should not be called in this version") From 52a395888ffb3e65e015041a7a23b602cc92f145 Mon Sep 17 00:00:00 2001 From: baekdohyeop Date: Mon, 29 Nov 2021 00:03:49 +0900 Subject: [PATCH 074/113] Create new dist option 'loadgroup' --- src/xdist/dsession.py | 2 + src/xdist/plugin.py | 10 ++- src/xdist/remote.py | 15 +++++ src/xdist/scheduler/__init__.py | 1 + src/xdist/scheduler/loadgroup.py | 67 +++++++++++++++++++ testing/acceptance_test.py | 109 +++++++++++++++++++++++++++++++ 6 files changed, 203 insertions(+), 1 deletion(-) create mode 100644 src/xdist/scheduler/loadgroup.py diff --git a/src/xdist/dsession.py b/src/xdist/dsession.py index 12539a00..eb5ae751 100644 --- a/src/xdist/dsession.py +++ b/src/xdist/dsession.py @@ -7,6 +7,7 @@ LoadScheduling, LoadScopeScheduling, LoadFileScheduling, + LoadGroupScheduling, ) @@ -100,6 +101,7 @@ def pytest_xdist_make_scheduler(self, config, log): "load": LoadScheduling, "loadscope": LoadScopeScheduling, "loadfile": LoadFileScheduling, + "loadgroup": LoadGroupScheduling, } return schedulers[dist](config, log) diff --git a/src/xdist/plugin.py b/src/xdist/plugin.py index 17be6d04..b406d0ba 100644 --- a/src/xdist/plugin.py +++ b/src/xdist/plugin.py @@ -86,7 +86,7 @@ def pytest_addoption(parser): "--dist", metavar="distmode", action="store", - choices=["each", "load", "loadscope", "loadfile", "no"], + choices=["each", "load", "loadscope", "loadfile", "loadgroup", "no"], dest="dist", default="no", help=( @@ -98,6 +98,8 @@ def pytest_addoption(parser): " the same scope to any available environment.\n\n" "loadfile: load balance by sending test grouped by file" " to any available environment.\n\n" + "loadgroup: load balance by sending any pending test or test group" + " to any available enviroment.\n\n" "(default) no: run tests inprocess, don't distribute." ), ) @@ -204,6 +206,12 @@ def pytest_configure(config): config.issue_config_time_warning(warning, 2) config.option.forked = True + config_line = ( + "xgroup: specify group for tests should run in same session." + "in relation to one another. " + "Provided by pytest-xdist." + ) + config.addinivalue_line("markers", config_line) + @pytest.hookimpl(tryfirst=True) def pytest_cmdline_main(config): diff --git a/src/xdist/remote.py b/src/xdist/remote.py index 914040d4..410d3ca9 100644 --- a/src/xdist/remote.py +++ b/src/xdist/remote.py @@ -116,6 +116,20 @@ def run_one_test(self, torun): "runtest_protocol_complete", item_index=self.item_index, duration=duration ) + def pytest_collection_modifyitems(self, session, config, items): + # add the group name to nodeid as suffix if --dist=loadgroup + if config.getvalue("loadgroup"): + for item in items: + try: + mark = item.get_closest_marker("xgroup") + except AttributeError: + mark = item.get_marker("xgroup") + + if mark: + gname = mark.kwargs.get("name") + if gname: + item._nodeid = "{}@{}".format(item.nodeid, gname) + @pytest.hookimpl def pytest_collection_finish(self, session): try: @@ -236,6 +250,7 @@ def remote_initconfig(option_dict, args): def setup_config(config, basetemp): + config.option.loadgroup = True if config.getvalue("dist") == "loadgroup" else False config.option.looponfail = False config.option.usepdb = False config.option.dist = "no" diff --git a/src/xdist/scheduler/__init__.py b/src/xdist/scheduler/__init__.py index 06ba6b7b..ab2e830f 100644 --- a/src/xdist/scheduler/__init__.py +++ b/src/xdist/scheduler/__init__.py @@ -2,3 +2,4 @@ from xdist.scheduler.load import LoadScheduling # noqa from xdist.scheduler.loadfile import LoadFileScheduling # noqa from xdist.scheduler.loadscope import LoadScopeScheduling # noqa +from xdist.scheduler.loadgroup import LoadGroupScheduling # noqa diff --git a/src/xdist/scheduler/loadgroup.py b/src/xdist/scheduler/loadgroup.py new file mode 100644 index 00000000..49951d89 --- /dev/null +++ b/src/xdist/scheduler/loadgroup.py @@ -0,0 +1,67 @@ +from .loadscope import LoadScopeScheduling +from py.log import Producer + + +class LoadGroupScheduling(LoadScopeScheduling): + """Implement load scheduling across nodes, but grouping test only has group mark. + + This distributes the tests collected across all nodes so each test is run + just once. All nodes collect and submit the list of tests and when all + collections are received it is verified they are identical collections. + Then the collection gets divided up in work units, grouped by group mark + (If there is no group mark, it is itself a group.), and those work units + et submitted to nodes. Whenever a node finishes an item, it calls + ``.mark_test_complete()`` which will trigger the scheduler to assign more + work units if the number of pending tests for the node falls below a low-watermark. + + When created, ``numnodes`` defines how many nodes are expected to submit a + collection. This is used to know when all nodes have finished collection. + + This class behaves very much like LoadScopeScheduling, + but with a itself or group(by marked) scope. + """ + + def __init__(self, config, log=None): + super().__init__(config, log) + if log is None: + self.log = Producer("loadgroupsched") + else: + self.log = log.loadgroupsched + + def _split_scope(self, nodeid): + """Determine the scope (grouping) of a nodeid. + + There are usually 3 cases for a nodeid:: + + example/loadsuite/test/test_beta.py::test_beta0 + example/loadsuite/test/test_delta.py::Delta1::test_delta0 + example/loadsuite/epsilon/__init__.py::epsilon.epsilon + + #. Function in a test module. + #. Method of a class in a test module. + #. Doctest in a function in a package. + + With loadgroup, two cases are added:: + + example/loadsuite/test/test_beta.py::test_beta0 + example/loadsuite/test/test_delta.py::Delta1::test_delta0 + example/loadsuite/epsilon/__init__.py::epsilon.epsilon + example/loadsuite/test/test_gamma.py::test_beta0@gname + example/loadsuite/test/test_delta.py::Gamma1::test_gamma0@gname + + This function will group tests with the scope determined by splitting + the first ``@`` from the right. That is, test will be grouped in a + single work unit when they have same group name. + In the above example, scopes will be:: + + example/loadsuite/test/test_beta.py::test_beta0 + example/loadsuite/test/test_delta.py::Delta1::test_delta0 + example/loadsuite/epsilon/__init__.py::epsilon.epsilon + gname + gname + """ + if nodeid.rfind("@") > nodeid.rfind("]"): + # check the index of ']' to avoid the case: parametrize mark value has '@' + return nodeid.split("@")[-1] + else: + return nodeid diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index 31d634bf..c7f99567 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -1326,6 +1326,115 @@ def test_2(): assert c1 == c2 +class TestGroupScope: + def test_by_module(self, testdir): + test_file = """ + import pytest + class TestA: + @pytest.mark.xgroup(name="xgroup") + @pytest.mark.parametrize('i', range(5)) + def test(self, i): + pass + """ + testdir.makepyfile(test_a=test_file, test_b=test_file) + result = testdir.runpytest("-n2", "--dist=loadgroup", "-v") + test_a_workers_and_test_count = get_workers_and_test_count_by_prefix( + "test_a.py::TestA", result.outlines + ) + test_b_workers_and_test_count = get_workers_and_test_count_by_prefix( + "test_b.py::TestA", result.outlines + ) + + assert ( + test_a_workers_and_test_count + in ( + {"gw0": 5}, + {"gw1": 0}, + ) + or test_a_workers_and_test_count in ({"gw0": 0}, {"gw1": 5}) + ) + assert ( + test_b_workers_and_test_count + in ( + {"gw0": 5}, + {"gw1": 0}, + ) + or test_b_workers_and_test_count in ({"gw0": 0}, {"gw1": 5}) + ) + assert ( + test_a_workers_and_test_count.items() + == test_b_workers_and_test_count.items() + ) + + def test_by_class(self, testdir): + testdir.makepyfile( + test_a=""" + import pytest + class TestA: + @pytest.mark.xgroup(name="xgroup") + @pytest.mark.parametrize('i', range(10)) + def test(self, i): + pass + class TestB: + @pytest.mark.xgroup(name="xgroup") + @pytest.mark.parametrize('i', range(10)) + def test(self, i): + pass + """ + ) + result = testdir.runpytest("-n2", "--dist=loadgroup", "-v") + test_a_workers_and_test_count = get_workers_and_test_count_by_prefix( + "test_a.py::TestA", result.outlines + ) + test_b_workers_and_test_count = get_workers_and_test_count_by_prefix( + "test_a.py::TestB", result.outlines + ) + + assert ( + test_a_workers_and_test_count + in ( + {"gw0": 10}, + {"gw1": 0}, + ) + or test_a_workers_and_test_count in ({"gw0": 0}, {"gw1": 10}) + ) + assert ( + test_b_workers_and_test_count + in ( + {"gw0": 10}, + {"gw1": 0}, + ) + or test_b_workers_and_test_count in ({"gw0": 0}, {"gw1": 10}) + ) + assert ( + test_a_workers_and_test_count.items() + == test_b_workers_and_test_count.items() + ) + + def test_module_single_start(self, testdir): + test_file1 = """ + import pytest + @pytest.mark.xgroup(name="xgroup") + def test(): + pass + """ + test_file2 = """ + import pytest + def test_1(): + pass + @pytest.mark.xgroup(name="xgroup") + def test_2(): + pass + """ + testdir.makepyfile(test_a=test_file1, test_b=test_file1, test_c=test_file2) + result = testdir.runpytest("-n2", "--dist=loadgroup", "-v") + a = get_workers_and_test_count_by_prefix("test_a.py::test", result.outlines) + b = get_workers_and_test_count_by_prefix("test_b.py::test", result.outlines) + c = get_workers_and_test_count_by_prefix("test_c.py::test_2", result.outlines) + + assert a.keys() == b.keys() and b.keys() == c.keys() + + class TestLocking: _test_content = """ class TestClassName%s(object): From 1b5d6b6db76d58a0c9985f6aeb634630590b06eb Mon Sep 17 00:00:00 2001 From: baekdohyeop Date: Mon, 29 Nov 2021 00:11:26 +0900 Subject: [PATCH 075/113] Add changelog --- changelog/733.feature.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog/733.feature.rst diff --git a/changelog/733.feature.rst b/changelog/733.feature.rst new file mode 100644 index 00000000..3ce8de0c --- /dev/null +++ b/changelog/733.feature.rst @@ -0,0 +1 @@ +Create new dist option 'loadgroup' From 62e50d00977b41e175b5f119381f9db760459ddc Mon Sep 17 00:00:00 2001 From: baekdohyeop Date: Mon, 29 Nov 2021 02:49:50 +0900 Subject: [PATCH 076/113] Address review --- README.rst | 22 ++++++++++++++++++++++ changelog/733.feature.rst | 2 +- src/xdist/plugin.py | 5 ++--- src/xdist/remote.py | 20 ++++++++++---------- src/xdist/scheduler/loadgroup.py | 25 ++++++------------------- testing/acceptance_test.py | 29 ++++++++++++++++++++++++----- 6 files changed, 65 insertions(+), 38 deletions(-) diff --git a/README.rst b/README.rst index 5768f7ca..c40c9f3e 100644 --- a/README.rst +++ b/README.rst @@ -96,6 +96,10 @@ distribution algorithm this with the ``--dist`` option. It takes these values: distributed to available workers as whole units. This guarantees that all tests in a file run in the same worker. +* ``--dist loadgroup``: Tests are grouped by xdist_group mark. Groups are + distributed to available workers as whole units. This guarantees that all + tests with same xdist_group name run in the same worker. + Making session-scoped fixtures execute only once ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -414,3 +418,21 @@ where the configuration file was found. .. _`pytest-xdist`: http://pypi.python.org/pypi/pytest-xdist .. _`pytest-xdist repository`: https://github.com/pytest-dev/pytest-xdist .. _`pytest`: http://pytest.org + +Groups tests by xdist_group mark +--------------------------------- + +*New in version 2.4.* + +Two or more tests belonging to different classes or modules can be executed in same worker through the xdist_group marker: + +.. code-block:: python + + @pytest.mark.xdist_group(name="group1") + def test1(): + pass + + class TestA: + @pytest.mark.xdist_group("group1") + def test2(): + pass diff --git a/changelog/733.feature.rst b/changelog/733.feature.rst index 3ce8de0c..28163e79 100644 --- a/changelog/733.feature.rst +++ b/changelog/733.feature.rst @@ -1 +1 @@ -Create new dist option 'loadgroup' +New ``--dist=loadgroup`` option, which ensures all tests marked with ``@pytest.mark.xdist_group`` run in the same session/worker. Other tests run distributed as in ``--dist=load``. diff --git a/src/xdist/plugin.py b/src/xdist/plugin.py index b406d0ba..85f76e82 100644 --- a/src/xdist/plugin.py +++ b/src/xdist/plugin.py @@ -98,8 +98,7 @@ def pytest_addoption(parser): " the same scope to any available environment.\n\n" "loadfile: load balance by sending test grouped by file" " to any available environment.\n\n" - "loadgroup: load balance by sending any pending test or test group" - " to any available enviroment.\n\n" + "loadgroup: like load, but sends tests marked with 'xdist_group' to the same worker.\n\n" "(default) no: run tests inprocess, don't distribute." ), ) @@ -207,7 +206,7 @@ def pytest_configure(config): config.option.forked = True config_line = ( - "xgroup: specify group for tests should run in same session." + "xdist_group: specify group for tests should run in same session." "in relation to one another. " + "Provided by pytest-xdist." ) config.addinivalue_line("markers", config_line) diff --git a/src/xdist/remote.py b/src/xdist/remote.py index 410d3ca9..160b042a 100644 --- a/src/xdist/remote.py +++ b/src/xdist/remote.py @@ -120,15 +120,15 @@ def pytest_collection_modifyitems(self, session, config, items): # add the group name to nodeid as suffix if --dist=loadgroup if config.getvalue("loadgroup"): for item in items: - try: - mark = item.get_closest_marker("xgroup") - except AttributeError: - mark = item.get_marker("xgroup") - - if mark: - gname = mark.kwargs.get("name") - if gname: - item._nodeid = "{}@{}".format(item.nodeid, gname) + mark = item.get_closest_marker("xdist_group") + if not mark: + continue + gname = ( + mark.args[0] + if len(mark.args) > 0 + else mark.kwargs.get("name", "default") + ) + item._nodeid = "{}@{}".format(item.nodeid, gname) @pytest.hookimpl def pytest_collection_finish(self, session): @@ -250,7 +250,7 @@ def remote_initconfig(option_dict, args): def setup_config(config, basetemp): - config.option.loadgroup = True if config.getvalue("dist") == "loadgroup" else False + config.option.loadgroup = config.getvalue("dist") == "loadgroup" config.option.looponfail = False config.option.usepdb = False config.option.dist = "no" diff --git a/src/xdist/scheduler/loadgroup.py b/src/xdist/scheduler/loadgroup.py index 49951d89..072f64ab 100644 --- a/src/xdist/scheduler/loadgroup.py +++ b/src/xdist/scheduler/loadgroup.py @@ -3,22 +3,10 @@ class LoadGroupScheduling(LoadScopeScheduling): - """Implement load scheduling across nodes, but grouping test only has group mark. + """Implement load scheduling across nodes, but grouping test by xdist_group mark. - This distributes the tests collected across all nodes so each test is run - just once. All nodes collect and submit the list of tests and when all - collections are received it is verified they are identical collections. - Then the collection gets divided up in work units, grouped by group mark - (If there is no group mark, it is itself a group.), and those work units - et submitted to nodes. Whenever a node finishes an item, it calls - ``.mark_test_complete()`` which will trigger the scheduler to assign more - work units if the number of pending tests for the node falls below a low-watermark. - - When created, ``numnodes`` defines how many nodes are expected to submit a - collection. This is used to know when all nodes have finished collection. - - This class behaves very much like LoadScopeScheduling, - but with a itself or group(by marked) scope. + This class behaves very much like LoadScopeScheduling, but it groups tests by xdist_group mark + instead of the module or class to which they belong to. """ def __init__(self, config, log=None): @@ -49,10 +37,9 @@ def _split_scope(self, nodeid): example/loadsuite/test/test_gamma.py::test_beta0@gname example/loadsuite/test/test_delta.py::Gamma1::test_gamma0@gname - This function will group tests with the scope determined by splitting - the first ``@`` from the right. That is, test will be grouped in a - single work unit when they have same group name. - In the above example, scopes will be:: + This function will group tests with the scope determined by splitting the first ``@`` + from the right. That is, test will be grouped in a single work unit when they have + same group name. In the above example, scopes will be:: example/loadsuite/test/test_beta.py::test_beta0 example/loadsuite/test/test_delta.py::Delta1::test_delta0 diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index c7f99567..c1391974 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -1331,7 +1331,7 @@ def test_by_module(self, testdir): test_file = """ import pytest class TestA: - @pytest.mark.xgroup(name="xgroup") + @pytest.mark.xdist_group(name="xdist_group") @pytest.mark.parametrize('i', range(5)) def test(self, i): pass @@ -1371,12 +1371,12 @@ def test_by_class(self, testdir): test_a=""" import pytest class TestA: - @pytest.mark.xgroup(name="xgroup") + @pytest.mark.xdist_group(name="xdist_group") @pytest.mark.parametrize('i', range(10)) def test(self, i): pass class TestB: - @pytest.mark.xgroup(name="xgroup") + @pytest.mark.xdist_group(name="xdist_group") @pytest.mark.parametrize('i', range(10)) def test(self, i): pass @@ -1414,7 +1414,7 @@ def test(self, i): def test_module_single_start(self, testdir): test_file1 = """ import pytest - @pytest.mark.xgroup(name="xgroup") + @pytest.mark.xdist_group(name="xdist_group") def test(): pass """ @@ -1422,7 +1422,7 @@ def test(): import pytest def test_1(): pass - @pytest.mark.xgroup(name="xgroup") + @pytest.mark.xdist_group(name="xdist_group") def test_2(): pass """ @@ -1434,6 +1434,25 @@ def test_2(): assert a.keys() == b.keys() and b.keys() == c.keys() + def test_with_two_group_names(self, testdir): + test_file = """ + import pytest + @pytest.mark.xdist_group(name="group1") + def test_1(): + pass + @pytest.mark.xdist_group("group2") + def test_2(): + pass + """ + testdir.makepyfile(test_a=test_file, test_b=test_file) + result = testdir.runpytest("-n2", "--dist=loadgroup", "-v") + a_1 = get_workers_and_test_count_by_prefix("test_a.py::test_1", result.outlines) + a_2 = get_workers_and_test_count_by_prefix("test_a.py::test_2", result.outlines) + b_1 = get_workers_and_test_count_by_prefix("test_b.py::test_1", result.outlines) + b_2 = get_workers_and_test_count_by_prefix("test_b.py::test_2", result.outlines) + + assert a_1.keys() == b_1.keys() and a_2.keys() == b_2.keys() + class TestLocking: _test_content = """ From 83bdbf4b95c914a889d1faa8fba8d506bcc2f8c7 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 29 Nov 2021 12:11:52 -0300 Subject: [PATCH 077/113] Revamp README * Use a document title. * Show a "short and sweet" section at the beginning highlighting the main usage of the plugin. * Add a table of contents. * Use a dedicated howto section at the end. * Move the OVERVIEW section to the main README. --- OVERVIEW.md | 76 ------------- README.rst | 314 +++++++++++++++++++++++++++++++++++----------------- 2 files changed, 210 insertions(+), 180 deletions(-) delete mode 100644 OVERVIEW.md diff --git a/OVERVIEW.md b/OVERVIEW.md deleted file mode 100644 index da0d3c4d..00000000 --- a/OVERVIEW.md +++ /dev/null @@ -1,76 +0,0 @@ -# Overview # - -`xdist` works by spawning one or more **workers**, which are controlled -by the **controller**. Each **worker** is responsible for performing -a full test collection and afterwards running tests as dictated by the **controller**. - -The execution flow is: - -1. **controller** spawns one or more **workers** at the beginning of - the test session. The communication between **controller** and **worker** nodes makes use of - [execnet](https://codespeak.net/execnet/) and its [gateways](https://codespeak.net/execnet/basics.html#gateways-bootstrapping-python-interpreters). - The actual interpreters executing the code for the **workers** might - be remote or local. - -1. Each **worker** itself is a mini pytest runner. **workers** at this - point perform a full test collection, sending back the collected - test-ids back to the **controller** which does not - perform any collection itself. - -1. The **controller** receives the result of the collection from all nodes. - At this point the **controller** performs some sanity check to ensure that - all **workers** collected the same tests (including order), bailing out otherwise. - If all is well, it converts the list of test-ids into a list of simple - indexes, where each index corresponds to the position of that test in the - original collection list. This works because all nodes have the same - collection list, and saves bandwidth because the **controller** can now tell - one of the workers to just *execute test index 3* index of passing the - full test id. - -1. If **dist-mode** is **each**: the **controller** just sends the full list - of test indexes to each node at this moment. - -1. If **dist-mode** is **load**: the **controller** takes around 25% of the - tests and sends them one by one to each **worker** in a round robin - fashion. The rest of the tests will be distributed later as **workers** - finish tests (see below). - -1. Note that `pytest_xdist_make_scheduler` hook can be used to implement custom tests distribution logic. - -1. **workers** re-implement `pytest_runtestloop`: pytest's default implementation - basically loops over all collected items in the `session` object and executes - the `pytest_runtest_protocol` for each test item, but in xdist **workers** sit idly - waiting for **controller** to send tests for execution. As tests are - received by **workers**, `pytest_runtest_protocol` is executed for each test. - Here it worth noting an implementation detail: **workers** always must keep at - least one test item on their queue due to how the `pytest_runtest_protocol(item, nextitem)` - hook is defined: in order to pass the `nextitem` to the hook, the worker must wait for more - instructions from controller before executing that remaining test. If it receives more tests, - then it can safely call `pytest_runtest_protocol` because it knows what the `nextitem` parameter will be. - If it receives a "shutdown" signal, then it can execute the hook passing `nextitem` as `None`. - -1. As tests are started and completed at the **workers**, the results are sent - back to the **controller**, which then just forwards the results to - the appropriate pytest hooks: `pytest_runtest_logstart` and - `pytest_runtest_logreport`. This way other plugins (for example `junitxml`) - can work normally. The **controller** (when in dist-mode **load**) - decides to send more tests to a node when a test completes, using - some heuristics such as test durations and how many tests each **worker** - still has to run. - -1. When the **controller** has no more pending tests it will - send a "shutdown" signal to all **workers**, which will then run their - remaining tests to completion and shut down. At this point the - **controller** will sit waiting for **workers** to shut down, still - processing events such as `pytest_runtest_logreport`. - -## FAQ ## - -> Why does each worker do its own collection, as opposed to having -the controller collect once and distribute from that collection to the workers? - -If collection was performed by controller then it would have to -serialize collected items to send them through the wire, as workers live in another process. -The problem is that test items are not easily (impossible?) to serialize, as they contain references to -the test functions, fixture managers, config objects, etc. Even if one manages to serialize it, -it seems it would be very hard to get it right and easy to break by any small change in pytest. diff --git a/README.rst b/README.rst index c40c9f3e..41f315e9 100644 --- a/README.rst +++ b/README.rst @@ -1,4 +1,6 @@ - +============ +pytest-xdist +============ .. image:: http://img.shields.io/pypi/v/pytest-xdist.svg :alt: PyPI version @@ -17,36 +19,17 @@ .. image:: https://img.shields.io/badge/code%20style-black-000000.svg :target: https://github.com/ambv/black -xdist: pytest distributed testing plugin -======================================== - -The `pytest-xdist`_ plugin extends pytest with some unique -test execution modes: +The `pytest-xdist`_ plugin extends pytest with new test execution modes, the most used being distributing +tests across multiple CPUs to speed up test execution:: -* test run parallelization_: if you have multiple CPUs or hosts you can use - those for a combined test run. This allows to speed up - development or to use special resources of `remote machines`_. - - -* ``--looponfail``: run your tests repeatedly in a subprocess. After each run - pytest waits until a file in your project changes and then re-runs - the previously failing tests. This is repeated until all tests pass - after which again a full run is performed. - -* `Multi-Platform`_ coverage: you can specify different Python interpreters - or different platforms and run tests in parallel on all of them. - -Before running tests remotely, ``pytest`` efficiently "rsyncs" your -program source code to the remote place. All test results -are reported back and displayed to your local terminal. -You may specify different Python versions and interpreters. - -If you would like to know how pytest-xdist works under the covers, checkout -`OVERVIEW `_. + pytest -n auto +With this call, pytest will spawn a number of workers processes equal to the number of available CPUs, and distribute +the tests randomly across them. There is also a number of `distribution modes`_ to choose from. **NOTE**: due to how pytest-xdist is implemented, the ``-s/--capture=no`` option does not work. +.. contents:: **Table of Contents** Installation ------------ @@ -61,29 +44,47 @@ To use ``psutil`` for detection of the number of CPUs available, install the ``p pip install pytest-xdist[psutil] +Features +-------- + +* Test run parallelization_: tests can be executed across multiple CPUs or hosts. + This allows to speed up development or to use special resources of `remote machines`_. + +* ``--looponfail``: run your tests repeatedly in a subprocess. After each run + pytest waits until a file in your project changes and then re-runs + the previously failing tests. This is repeated until all tests pass + after which again a full run is performed. + +* `Multi-Platform`_ coverage: you can specify different Python interpreters + or different platforms and run tests in parallel on all of them. + + Before running tests remotely, ``pytest`` efficiently "rsyncs" your + program source code to the remote place. + You may specify different Python versions and interpreters. It does not + installs/synchronize dependencies however. + + **Note**: this mode exists mostly for backward compatibility, as modern development + relies on continuous integration for multi-platform testing. + .. _parallelization: -Speed up test runs by sending tests to multiple CPUs ----------------------------------------------------- +Running tests across multiple CPUs +---------------------------------- To send tests to multiple CPUs, use the ``-n`` (or ``--numprocesses``) option:: - pytest -n NUMCPUS + pytest -n 8 Pass ``-n auto`` to use as many processes as your computer has CPU cores. This can lead to considerable speed ups, especially if your test suite takes a noticeable amount of time. -If a test crashes a worker, pytest-xdist will automatically restart that worker -and report the test’s failure. You can use the ``--max-worker-restart`` option -to limit the number of worker restarts that are allowed, or disable restarting -altogether using ``--max-worker-restart 0``. +The test distribution algorithm is configured with the ``--dist`` command-line option: -By default, using ``--numprocesses`` will send pending tests to any worker that -is available, without any guaranteed order. You can change the test -distribution algorithm this with the ``--dist`` option. It takes these values: +.. _distribution modes: -* ``--dist no``: The default algorithm, distributing one test at a time. +* ``--dist load`` **(default)**: Sends pending tests to any worker that is + available, without any guaranteed order. * ``--dist loadscope``: Tests are grouped by **module** for *test functions* and by **class** for *test methods*. Groups are distributed to available @@ -96,67 +97,31 @@ distribution algorithm this with the ``--dist`` option. It takes these values: distributed to available workers as whole units. This guarantees that all tests in a file run in the same worker. -* ``--dist loadgroup``: Tests are grouped by xdist_group mark. Groups are +* ``--dist loadgroup``: Tests are grouped by the ``xdist_group`` mark. Groups are distributed to available workers as whole units. This guarantees that all - tests with same xdist_group name run in the same worker. - -Making session-scoped fixtures execute only once -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -``pytest-xdist`` is designed so that each worker process will perform its own collection and execute -a subset of all tests. This means that tests in different processes requesting a high-level -scoped fixture (for example ``session``) will execute the fixture code more than once, which -breaks expectations and might be undesired in certain situations. - -While ``pytest-xdist`` does not have a builtin support for ensuring a session-scoped fixture is -executed exactly once, this can be achieved by using a lock file for inter-process communication. - -The example below needs to execute the fixture ``session_data`` only once (because it is -resource intensive, or needs to execute only once to define configuration options, etc), so it makes -use of a `FileLock `_ to produce the fixture data only once -when the first process requests the fixture, while the other processes will then read -the data from a file. - -Here is the code: - -.. code-block:: python - - import json + tests with same ``xdist_group`` name run in the same worker. - import pytest - from filelock import FileLock + .. code-block:: python + @pytest.mark.xdist_group(name="group1") + def test1(): + pass - @pytest.fixture(scope="session") - def session_data(tmp_path_factory, worker_id): - if worker_id == "master": - # not executing in with multiple workers, just produce the data and let - # pytest's fixture caching do its job - return produce_expensive_data() - - # get the temp directory shared by all workers - root_tmp_dir = tmp_path_factory.getbasetemp().parent - - fn = root_tmp_dir / "data.json" - with FileLock(str(fn) + ".lock"): - if fn.is_file(): - data = json.loads(fn.read_text()) - else: - data = produce_expensive_data() - fn.write_text(json.dumps(data)) - return data + class TestA: + @pytest.mark.xdist_group("group1") + def test2(): + pass + This will make sure ``test1`` and ``TestA::test2`` will run in the same worker. + Tests without the ``xdist_group`` mark are distributed normally as in the ``--dist=load`` mode. -The example above can also be use in cases a fixture needs to execute exactly once per test session, like -initializing a database service and populating initial tables. +* ``--dist no``: The normal pytest execution mode, runs one test at a time (no distribution at all). -This technique might not work for every case, but should be a starting point for many situations -where executing a high-scope fixture exactly once is important. Running tests in a Python subprocess ------------------------------------ -To instantiate a python3.9 subprocess and send tests to it, you may type:: +To instantiate a ``python3.9`` subprocess and send tests to it, you may type:: pytest -d --tx popen//python=python3.9 @@ -253,8 +218,21 @@ at once. The specifications strings use the `xspec syntax`_. .. _`execnet`: https://codespeak.net/execnet + +When tests crash +---------------- + +If a test crashes a worker, pytest-xdist will automatically restart that worker +and report the test’s failure. You can use the ``--max-worker-restart`` option +to limit the number of worker restarts that are allowed, or disable restarting +altogether using ``--max-worker-restart 0``. + + +How-tos +------- + Identifying the worker process during a test --------------------------------------------- +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ *New in version 1.15.* @@ -315,7 +293,7 @@ Since version 2.0, the following functions are also available in the ``xdist`` m Identifying workers from the system environment ------------------------------------------------ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ *New in version 2.4* @@ -335,7 +313,7 @@ external scripts. Uniquely identifying the current test run ------------------------------------------ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ *New in version 1.32.* @@ -369,14 +347,14 @@ Additionally, during a test run, the following environment variable is defined: * ``PYTEST_XDIST_TESTRUNUID``: the unique id of the test run. Accessing ``sys.argv`` from the controller node in workers ----------------------------------------------------------- +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ To access the ``sys.argv`` passed to the command-line of the controller node, use ``request.config.workerinput["mainargv"]``. Specifying test exec environments in an ini file ------------------------------------------------- +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ You can use pytest's ini file configuration to avoid typing common options. You can for example make running with three subprocesses your default like this: @@ -401,7 +379,7 @@ to run tests in each of the environments. Specifying "rsync" dirs in an ini-file --------------------------------------- +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ In a ``tox.ini`` or ``setup.cfg`` file in your root project directory you may specify directories to include or to exclude in synchronisation: @@ -419,20 +397,148 @@ where the configuration file was found. .. _`pytest-xdist repository`: https://github.com/pytest-dev/pytest-xdist .. _`pytest`: http://pytest.org -Groups tests by xdist_group mark ---------------------------------- -*New in version 2.4.* +Making session-scoped fixtures execute only once +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +``pytest-xdist`` is designed so that each worker process will perform its own collection and execute +a subset of all tests. This means that tests in different processes requesting a high-level +scoped fixture (for example ``session``) will execute the fixture code more than once, which +breaks expectations and might be undesired in certain situations. + +While ``pytest-xdist`` does not have a builtin support for ensuring a session-scoped fixture is +executed exactly once, this can be achieved by using a lock file for inter-process communication. + +The example below needs to execute the fixture ``session_data`` only once (because it is +resource intensive, or needs to execute only once to define configuration options, etc), so it makes +use of a `FileLock `_ to produce the fixture data only once +when the first process requests the fixture, while the other processes will then read +the data from a file. -Two or more tests belonging to different classes or modules can be executed in same worker through the xdist_group marker: +Here is the code: .. code-block:: python - @pytest.mark.xdist_group(name="group1") - def test1(): - pass + import json + + import pytest + from filelock import FileLock + + + @pytest.fixture(scope="session") + def session_data(tmp_path_factory, worker_id): + if worker_id == "master": + # not executing in with multiple workers, just produce the data and let + # pytest's fixture caching do its job + return produce_expensive_data() + + # get the temp directory shared by all workers + root_tmp_dir = tmp_path_factory.getbasetemp().parent + + fn = root_tmp_dir / "data.json" + with FileLock(str(fn) + ".lock"): + if fn.is_file(): + data = json.loads(fn.read_text()) + else: + data = produce_expensive_data() + fn.write_text(json.dumps(data)) + return data + + +The example above can also be use in cases a fixture needs to execute exactly once per test session, like +initializing a database service and populating initial tables. + +This technique might not work for every case, but should be a starting point for many situations +where executing a high-scope fixture exactly once is important. + - class TestA: - @pytest.mark.xdist_group("group1") - def test2(): - pass +How does xdist work? +-------------------- + +``xdist`` works by spawning one or more **workers**, which are +controlled by the **controller**. Each **worker** is responsible for +performing a full test collection and afterwards running tests as +dictated by the **controller**. + +The execution flow is: + +1. **controller** spawns one or more **workers** at the beginning of the + test session. The communication between **controller** and **worker** + nodes makes use of `execnet `__ and + its + `gateways `__. + The actual interpreters executing the code for the **workers** might + be remote or local. + +2. Each **worker** itself is a mini pytest runner. **workers** at this + point perform a full test collection, sending back the collected + test-ids back to the **controller** which does not perform any + collection itself. + +3. The **controller** receives the result of the collection from all + nodes. At this point the **controller** performs some sanity check to + ensure that all **workers** collected the same tests (including + order), bailing out otherwise. If all is well, it converts the list + of test-ids into a list of simple indexes, where each index + corresponds to the position of that test in the original collection + list. This works because all nodes have the same collection list, and + saves bandwidth because the **controller** can now tell one of the + workers to just *execute test index 3* index of passing the full test + id. + +4. If **dist-mode** is **each**: the **controller** just sends the full + list of test indexes to each node at this moment. + +5. If **dist-mode** is **load**: the **controller** takes around 25% of + the tests and sends them one by one to each **worker** in a round + robin fashion. The rest of the tests will be distributed later as + **workers** finish tests (see below). + +6. Note that ``pytest_xdist_make_scheduler`` hook can be used to + implement custom tests distribution logic. + +7. **workers** re-implement ``pytest_runtestloop``: pytest’s default + implementation basically loops over all collected items in the + ``session`` object and executes the ``pytest_runtest_protocol`` for + each test item, but in xdist **workers** sit idly waiting for + **controller** to send tests for execution. As tests are received by + **workers**, ``pytest_runtest_protocol`` is executed for each test. + Here it worth noting an implementation detail: **workers** always + must keep at least one test item on their queue due to how the + ``pytest_runtest_protocol(item, nextitem)`` hook is defined: in order + to pass the ``nextitem`` to the hook, the worker must wait for more + instructions from controller before executing that remaining test. If + it receives more tests, then it can safely call + ``pytest_runtest_protocol`` because it knows what the ``nextitem`` + parameter will be. If it receives a “shutdown” signal, then it can + execute the hook passing ``nextitem`` as ``None``. + +8. As tests are started and completed at the **workers**, the results + are sent back to the **controller**, which then just forwards the + results to the appropriate pytest hooks: ``pytest_runtest_logstart`` + and ``pytest_runtest_logreport``. This way other plugins (for example + ``junitxml``) can work normally. The **controller** (when in + dist-mode **load**) decides to send more tests to a node when a test + completes, using some heuristics such as test durations and how many + tests each **worker** still has to run. + +9. When the **controller** has no more pending tests it will send a + “shutdown” signal to all **workers**, which will then run their + remaining tests to completion and shut down. At this point the + **controller** will sit waiting for **workers** to shut down, still + processing events such as ``pytest_runtest_logreport``. + +FAQ +--- + +**Question**: Why does each worker do its own collection, as opposed to having the +controller collect once and distribute from that collection to the +workers? + +If collection was performed by controller then it would have to +serialize collected items to send them through the wire, as workers live +in another process. The problem is that test items are not easily +(impossible?) to serialize, as they contain references to the test +functions, fixture managers, config objects, etc. Even if one manages to +serialize it, it seems it would be very hard to get it right and easy to +break by any small change in pytest. From a25c14bef59ad728e39cabc64f71190aaad73b0a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 6 Dec 2021 21:11:40 +0000 Subject: [PATCH 078/113] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/psf/black: 21.11b1 → 21.12b0](https://github.com/psf/black/compare/21.11b1...21.12b0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7a42be96..492dbc64 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/psf/black - rev: 21.11b1 + rev: 21.12b0 hooks: - id: black args: [--safe, --quiet, --target-version, py35] From c8bbc03e49d5a53b5da808c7328e8f3ad6ed2d7e Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 10 Dec 2021 08:18:22 -0300 Subject: [PATCH 079/113] Release 2.5.0 --- CHANGELOG.rst | 21 +++++++++++++++++++++ changelog/708.trivial.rst | 1 - changelog/719.trivial.rst | 1 - changelog/720.trivial.rst | 1 - changelog/721.trivial.rst | 1 - changelog/722.feature.rst | 1 - changelog/733.feature.rst | 1 - 7 files changed, 21 insertions(+), 6 deletions(-) delete mode 100644 changelog/708.trivial.rst delete mode 100644 changelog/719.trivial.rst delete mode 100644 changelog/720.trivial.rst delete mode 100644 changelog/721.trivial.rst delete mode 100644 changelog/722.feature.rst delete mode 100644 changelog/733.feature.rst diff --git a/CHANGELOG.rst b/CHANGELOG.rst index e302e069..452dd686 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,3 +1,24 @@ +pytest-xdist 2.5.0 (2021-12-10) +Features +-------- + +- `#722 `_: Full compatibility with pytest 7 - no deprecation warnings or use of legacy features. + +- `#733 `_: New ``--dist=loadgroup`` option, which ensures all tests marked with ``@pytest.mark.xdist_group`` run in the same session/worker. Other tests run distributed as in ``--dist=load``. + + +Trivial Changes +--------------- + +- `#708 `_: Use ``@pytest.hookspec`` decorator to declare hook options in ``newhooks.py`` to avoid warnings in ``pytest 7.0``. + +- `#719 `_: Use up-to-date ``setup.cfg``/``pyproject.toml`` packaging setup. + +- `#720 `_: Require pytest>=6.2.0. + +- `#721 `_: Started using type annotations and mypy checking internally. The types are incomplete and not published. + + pytest-xdist 2.4.0 (2021-09-20) =============================== diff --git a/changelog/708.trivial.rst b/changelog/708.trivial.rst deleted file mode 100644 index 38b41769..00000000 --- a/changelog/708.trivial.rst +++ /dev/null @@ -1 +0,0 @@ -Use ``@pytest.hookspec`` decorator to declare hook options in ``newhooks.py`` to avoid warnings in ``pytest 7.0``. diff --git a/changelog/719.trivial.rst b/changelog/719.trivial.rst deleted file mode 100644 index 8fdb3b65..00000000 --- a/changelog/719.trivial.rst +++ /dev/null @@ -1 +0,0 @@ -Use up-to-date ``setup.cfg``/``pyproject.toml`` packaging setup. diff --git a/changelog/720.trivial.rst b/changelog/720.trivial.rst deleted file mode 100644 index ee671cbb..00000000 --- a/changelog/720.trivial.rst +++ /dev/null @@ -1 +0,0 @@ -Require pytest>=6.2.0. diff --git a/changelog/721.trivial.rst b/changelog/721.trivial.rst deleted file mode 100644 index 1c32376c..00000000 --- a/changelog/721.trivial.rst +++ /dev/null @@ -1 +0,0 @@ -Started using type annotations and mypy checking internally. The types are incomplete and not published. diff --git a/changelog/722.feature.rst b/changelog/722.feature.rst deleted file mode 100644 index b8654ffb..00000000 --- a/changelog/722.feature.rst +++ /dev/null @@ -1 +0,0 @@ -Full compatibility with pytest 7 - no deprecation warnings or use of legacy features. diff --git a/changelog/733.feature.rst b/changelog/733.feature.rst deleted file mode 100644 index 28163e79..00000000 --- a/changelog/733.feature.rst +++ /dev/null @@ -1 +0,0 @@ -New ``--dist=loadgroup`` option, which ensures all tests marked with ``@pytest.mark.xdist_group`` run in the same session/worker. Other tests run distributed as in ``--dist=load``. From 5f78c7155e66ab73bdc7631c4ac6bfe684b82500 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 10 Dec 2021 08:19:30 -0300 Subject: [PATCH 080/113] Fix CHANGELOG header --- CHANGELOG.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 452dd686..e9d54a2a 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,4 +1,6 @@ pytest-xdist 2.5.0 (2021-12-10) +=============================== + Features -------- From c76d5622f17135e892965d742377870eb9b07933 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 10 Dec 2021 08:31:00 -0300 Subject: [PATCH 081/113] Skip test_warning_captured_deprecated_in_pytest_6 in pytest>=7.1 --- src/xdist/dsession.py | 2 ++ testing/acceptance_test.py | 7 +++++++ 2 files changed, 9 insertions(+) diff --git a/src/xdist/dsession.py b/src/xdist/dsession.py index eb5ae751..2ae3db6b 100644 --- a/src/xdist/dsession.py +++ b/src/xdist/dsession.py @@ -294,6 +294,8 @@ def worker_collectreport(self, node, rep): def worker_warning_captured(self, warning_message, when, item): """Emitted when a node calls the pytest_warning_captured hook (deprecated in 6.0).""" + # This hook as been removed in pytest 7.1, and we can remove support once we only + # support pytest >=7.1. kwargs = dict(warning_message=warning_message, when=when, item=item) self.config.hook.pytest_warning_captured.call_historic(kwargs=kwargs) diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index c1391974..68370916 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -773,6 +773,13 @@ def test_warning_captured_deprecated_in_pytest_6( """ Do not trigger the deprecated pytest_warning_captured hook in pytest 6+ (#562) """ + from _pytest import hookspec + + if not hasattr(hookspec, "pytest_warning_captured"): + pytest.skip( + f"pytest {pytest.__version__} does not have the pytest_warning_captured hook." + ) + pytester.makeconftest( """ def pytest_warning_captured(warning_message): From 13f39349c6950a881c1fe4fcd5984af2e8b7c220 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 10 Dec 2021 08:32:32 -0300 Subject: [PATCH 082/113] Remove unnecessary skip from test_logfinish_hook as we require pytest>=6.2 --- testing/acceptance_test.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index 68370916..dee18489 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -447,11 +447,6 @@ def test_func(): def test_logfinish_hook(self, pytester: pytest.Pytester) -> None: """Ensure the pytest_runtest_logfinish hook is being properly handled""" - from _pytest import hookspec - - if not hasattr(hookspec, "pytest_runtest_logfinish"): - pytest.skip("test requires pytest_runtest_logfinish hook in pytest (3.4+)") - pytester.makeconftest( """ def pytest_runtest_logfinish(): From 82ecdea0c2b9ed0fa0d6b00828eb5e0e8410caf5 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 20 Dec 2021 20:42:50 +0000 Subject: [PATCH 083/113] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-mypy: v0.910-1 → v0.920](https://github.com/pre-commit/mirrors-mypy/compare/v0.910-1...v0.920) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 492dbc64..3ac4ba36 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -29,7 +29,7 @@ repos: language: python additional_dependencies: [pygments, restructuredtext_lint] - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.910-1 + rev: v0.920 hooks: - id: mypy files: ^(src/|testing/) From d21f59fea72d2fe5c60db0f7d1def2a06c053451 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 27 Dec 2021 08:05:01 -0300 Subject: [PATCH 084/113] Fix typing errors after updating mypy --- testing/test_looponfail.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/testing/test_looponfail.py b/testing/test_looponfail.py index 02a1f592..cd1cc43e 100644 --- a/testing/test_looponfail.py +++ b/testing/test_looponfail.py @@ -1,3 +1,5 @@ +from typing import cast + import py import pytest import shutil @@ -123,7 +125,7 @@ def test_failures_somewhere(self, pytester: pytest.Pytester) -> None: failures = control.runsession() assert failures control.setup() - item_path = item.path if PYTEST_GTE_7 else Path(item.fspath) # type: ignore[attr-defined] + item_path = item.path if PYTEST_GTE_7 else Path(cast(py.path.local, item.fspath)) # type: ignore[attr-defined] item_path.write_text("def test_func():\n assert 1\n") removepyc(item_path) topdir, failures = control.runsession()[:2] @@ -141,7 +143,11 @@ def test_func(): control = RemoteControl(modcol.config) control.loop_once() assert control.failures - modcol_path = modcol.path if PYTEST_GTE_7 else Path(modcol.fspath) # type: ignore[attr-defined] + if PYTEST_GTE_7: + modcol_path = modcol.path # type:ignore[attr-defined] + else: + modcol_path = Path(cast(py.path.local, modcol.fspath)) + modcol_path.write_text( textwrap.dedent( """ @@ -173,7 +179,7 @@ def test_func(): if PYTEST_GTE_7: parent = modcol.path.parent.parent # type: ignore[attr-defined] else: - parent = Path(modcol.fspath.dirpath().dirpath()) + parent = Path(cast(py.path.local, modcol.fspath).dirpath().dirpath()) monkeypatch.chdir(parent) modcol.config.args = [ str(Path(x).relative_to(parent)) for x in modcol.config.args From 18143275c63a5d2ceb54afebbb944dbc0e7ac9f1 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 27 Dec 2021 08:05:46 -0300 Subject: [PATCH 085/113] Remove obsolete skip based on java (no longer supported) --- testing/acceptance_test.py | 1 - 1 file changed, 1 deletion(-) diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index dee18489..44443d03 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -154,7 +154,6 @@ def test_skip(): result.stdout.fnmatch_lines(["*2*Python*", "*2 failed, 1 passed, 1 skipped*"]) assert result.ret == 1 - @pytest.mark.xfail("sys.platform.startswith('java')", run=False) def test_dist_tests_with_crash(self, pytester: pytest.Pytester) -> None: if not hasattr(os, "kill"): pytest.skip("no os.kill") From 1c7fc1a812b63f5b91a3f67095d746415c3e2540 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 27 Dec 2021 21:13:26 +0000 Subject: [PATCH 086/113] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/pre-commit-hooks: v4.0.1 → v4.1.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.0.1...v4.1.0) - [github.com/pre-commit/mirrors-mypy: v0.920 → v0.930](https://github.com/pre-commit/mirrors-mypy/compare/v0.920...v0.930) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3ac4ba36..08fec617 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -5,7 +5,7 @@ repos: - id: black args: [--safe, --quiet, --target-version, py35] - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.0.1 + rev: v4.1.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer @@ -29,7 +29,7 @@ repos: language: python additional_dependencies: [pygments, restructuredtext_lint] - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.920 + rev: v0.930 hooks: - id: mypy files: ^(src/|testing/) From c4e0d39f217d346b64f98a9a38a3898fd498f650 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 3 Jan 2022 21:09:27 +0000 Subject: [PATCH 087/113] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v2.29.1 → v2.31.0](https://github.com/asottile/pyupgrade/compare/v2.29.1...v2.31.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 08fec617..72e8c964 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -16,7 +16,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/asottile/pyupgrade - rev: v2.29.1 + rev: v2.31.0 hooks: - id: pyupgrade args: [--py3-plus] From 628a04de8e2f76a09a86dd8f0c94d2e3c126b9f5 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 10 Jan 2022 21:28:28 +0000 Subject: [PATCH 088/113] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-mypy: v0.930 → v0.931](https://github.com/pre-commit/mirrors-mypy/compare/v0.930...v0.931) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 72e8c964..223427dc 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -29,7 +29,7 @@ repos: language: python additional_dependencies: [pygments, restructuredtext_lint] - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.930 + rev: v0.931 hooks: - id: mypy files: ^(src/|testing/) From 2aeb5d51f58c06b3d7c0be2a8e1e586a7bb15b55 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Mon, 24 Jan 2022 16:14:24 +0200 Subject: [PATCH 089/113] CHANGELOG: add `--boxed` deprecation to 2.5.0 --- CHANGELOG.rst | 6 ++++++ changelog/468.deprecation.rst | 3 --- 2 files changed, 6 insertions(+), 3 deletions(-) delete mode 100644 changelog/468.deprecation.rst diff --git a/CHANGELOG.rst b/CHANGELOG.rst index e9d54a2a..c20908c2 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,12 @@ pytest-xdist 2.5.0 (2021-12-10) =============================== +Deprecations and Removals +------------------------- + +- `#468 `_: The ``--boxed`` commmand line argument is deprecated. Install pytest-forked and use ``--forked`` instead. pytest-xdist 3.0.0 will remove the ``--boxed`` argument and pytest-forked dependency. + + Features -------- diff --git a/changelog/468.deprecation.rst b/changelog/468.deprecation.rst deleted file mode 100644 index e3109c9e..00000000 --- a/changelog/468.deprecation.rst +++ /dev/null @@ -1,3 +0,0 @@ -The ``--boxed`` commmand line argument is deprecated. -Install pytest-forked and use ``--forked`` instead. -pytest-xdist 3.0.0 will remove the ``--boxed`` argument and pytest-forked dependency. From dd61dce892fb5f09089300862f77c39ea18804eb Mon Sep 17 00:00:00 2001 From: Sorin Sbarnea Date: Sat, 29 Jan 2022 10:44:42 +0000 Subject: [PATCH 090/113] Document two missing arguments Fixes: #614 --- README.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.rst b/README.rst index 41f315e9..73f00f4e 100644 --- a/README.rst +++ b/README.rst @@ -79,6 +79,12 @@ Pass ``-n auto`` to use as many processes as your computer has CPU cores. This can lead to considerable speed ups, especially if your test suite takes a noticeable amount of time. +* ``--maxprocesses=maxprocesses``: limit the maximum number of workers to + process the tests. + +* ``--max-worker-restart``: maximum number of workers that can be restarted + when crashed (set to zero to disable this feature). + The test distribution algorithm is configured with the ``--dist`` command-line option: .. _distribution modes: From 8cd10225dfd6a417ad3218a119f65b87f71c5330 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 29 Jan 2022 13:29:36 -0300 Subject: [PATCH 091/113] Add sphinx docs folder and changelog URLs to metadata (#751) --- README.rst | 525 +----------------------------------------- docs/.gitignore | 1 + docs/boxed.rst | 9 + docs/changelog.rst | 5 + docs/conf.py | 54 +++++ docs/crash.rst | 7 + docs/distribution.rst | 56 +++++ docs/how-it-works.rst | 90 ++++++++ docs/how-to.rst | 224 ++++++++++++++++++ docs/index.rst | 63 +++++ docs/remote.rst | 72 ++++++ docs/subprocess.rst | 16 ++ setup.cfg | 5 + tox.ini | 9 + 14 files changed, 615 insertions(+), 521 deletions(-) create mode 100644 docs/.gitignore create mode 100644 docs/boxed.rst create mode 100644 docs/changelog.rst create mode 100644 docs/conf.py create mode 100644 docs/crash.rst create mode 100644 docs/distribution.rst create mode 100644 docs/how-it-works.rst create mode 100644 docs/how-to.rst create mode 100644 docs/index.rst create mode 100644 docs/remote.rst create mode 100644 docs/subprocess.rst diff --git a/README.rst b/README.rst index 73f00f4e..176fcba1 100644 --- a/README.rst +++ b/README.rst @@ -25,526 +25,9 @@ tests across multiple CPUs to speed up test execution:: pytest -n auto With this call, pytest will spawn a number of workers processes equal to the number of available CPUs, and distribute -the tests randomly across them. There is also a number of `distribution modes`_ to choose from. +the tests randomly across them. -**NOTE**: due to how pytest-xdist is implemented, the ``-s/--capture=no`` option does not work. +Documentation +============= -.. contents:: **Table of Contents** - -Installation ------------- - -Install the plugin with:: - - pip install pytest-xdist - - -To use ``psutil`` for detection of the number of CPUs available, install the ``psutil`` extra:: - - pip install pytest-xdist[psutil] - - -Features --------- - -* Test run parallelization_: tests can be executed across multiple CPUs or hosts. - This allows to speed up development or to use special resources of `remote machines`_. - -* ``--looponfail``: run your tests repeatedly in a subprocess. After each run - pytest waits until a file in your project changes and then re-runs - the previously failing tests. This is repeated until all tests pass - after which again a full run is performed. - -* `Multi-Platform`_ coverage: you can specify different Python interpreters - or different platforms and run tests in parallel on all of them. - - Before running tests remotely, ``pytest`` efficiently "rsyncs" your - program source code to the remote place. - You may specify different Python versions and interpreters. It does not - installs/synchronize dependencies however. - - **Note**: this mode exists mostly for backward compatibility, as modern development - relies on continuous integration for multi-platform testing. - -.. _parallelization: - -Running tests across multiple CPUs ----------------------------------- - -To send tests to multiple CPUs, use the ``-n`` (or ``--numprocesses``) option:: - - pytest -n 8 - -Pass ``-n auto`` to use as many processes as your computer has CPU cores. This -can lead to considerable speed ups, especially if your test suite takes a -noticeable amount of time. - -* ``--maxprocesses=maxprocesses``: limit the maximum number of workers to - process the tests. - -* ``--max-worker-restart``: maximum number of workers that can be restarted - when crashed (set to zero to disable this feature). - -The test distribution algorithm is configured with the ``--dist`` command-line option: - -.. _distribution modes: - -* ``--dist load`` **(default)**: Sends pending tests to any worker that is - available, without any guaranteed order. - -* ``--dist loadscope``: Tests are grouped by **module** for *test functions* - and by **class** for *test methods*. Groups are distributed to available - workers as whole units. This guarantees that all tests in a group run in the - same process. This can be useful if you have expensive module-level or - class-level fixtures. Grouping by class takes priority over grouping by - module. - -* ``--dist loadfile``: Tests are grouped by their containing file. Groups are - distributed to available workers as whole units. This guarantees that all - tests in a file run in the same worker. - -* ``--dist loadgroup``: Tests are grouped by the ``xdist_group`` mark. Groups are - distributed to available workers as whole units. This guarantees that all - tests with same ``xdist_group`` name run in the same worker. - - .. code-block:: python - - @pytest.mark.xdist_group(name="group1") - def test1(): - pass - - class TestA: - @pytest.mark.xdist_group("group1") - def test2(): - pass - - This will make sure ``test1`` and ``TestA::test2`` will run in the same worker. - Tests without the ``xdist_group`` mark are distributed normally as in the ``--dist=load`` mode. - -* ``--dist no``: The normal pytest execution mode, runs one test at a time (no distribution at all). - - -Running tests in a Python subprocess ------------------------------------- - -To instantiate a ``python3.9`` subprocess and send tests to it, you may type:: - - pytest -d --tx popen//python=python3.9 - -This will start a subprocess which is run with the ``python3.9`` -Python interpreter, found in your system binary lookup path. - -If you prefix the --tx option value like this:: - - --tx 3*popen//python=python3.9 - -then three subprocesses would be created and tests -will be load-balanced across these three processes. - -.. _boxed: - -Running tests in a boxed subprocess ------------------------------------ - -This functionality has been moved to the -`pytest-forked `_ plugin, but the ``--boxed`` option -is still kept for backward compatibility. - -.. _`remote machines`: - -Sending tests to remote SSH accounts ------------------------------------- - -Suppose you have a package ``mypkg`` which contains some -tests that you can successfully run locally. And you -have a ssh-reachable machine ``myhost``. Then -you can ad-hoc distribute your tests by typing:: - - pytest -d --tx ssh=myhostpopen --rsyncdir mypkg mypkg - -This will synchronize your :code:`mypkg` package directory -to a remote ssh account and then locally collect tests -and send them to remote places for execution. - -You can specify multiple :code:`--rsyncdir` directories -to be sent to the remote side. - -.. note:: - - For pytest to collect and send tests correctly - you not only need to make sure all code and tests - directories are rsynced, but that any test (sub) directory - also has an :code:`__init__.py` file because internally - pytest references tests as a fully qualified python - module path. **You will otherwise get strange errors** - during setup of the remote side. - - -You can specify multiple :code:`--rsyncignore` glob patterns -to be ignored when file are sent to the remote side. -There are also internal ignores: :code:`.*, *.pyc, *.pyo, *~` -Those you cannot override using rsyncignore command-line or -ini-file option(s). - - -Sending tests to remote Socket Servers --------------------------------------- - -Download the single-module `socketserver.py`_ Python program -and run it like this:: - - python socketserver.py - -It will tell you that it starts listening on the default -port. You can now on your home machine specify this -new socket host with something like this:: - - pytest -d --tx socket=192.168.1.102:8888 --rsyncdir mypkg mypkg - - -.. _`atonce`: -.. _`Multi-Platform`: - - -Running tests on many platforms at once ---------------------------------------- - -The basic command to run tests on multiple platforms is:: - - pytest --dist=each --tx=spec1 --tx=spec2 - -If you specify a windows host, an OSX host and a Linux -environment this command will send each tests to all -platforms - and report back failures from all platforms -at once. The specifications strings use the `xspec syntax`_. - -.. _`xspec syntax`: https://codespeak.net/execnet/basics.html#xspec - -.. _`socketserver.py`: https://raw.githubusercontent.com/pytest-dev/execnet/master/execnet/script/socketserver.py - -.. _`execnet`: https://codespeak.net/execnet - - -When tests crash ----------------- - -If a test crashes a worker, pytest-xdist will automatically restart that worker -and report the test’s failure. You can use the ``--max-worker-restart`` option -to limit the number of worker restarts that are allowed, or disable restarting -altogether using ``--max-worker-restart 0``. - - -How-tos -------- - -Identifying the worker process during a test -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -*New in version 1.15.* - -If you need to determine the identity of a worker process in -a test or fixture, you may use the ``worker_id`` fixture to do so: - -.. code-block:: python - - @pytest.fixture() - def user_account(worker_id): - """ use a different account in each xdist worker """ - return "account_%s" % worker_id - -When ``xdist`` is disabled (running with ``-n0`` for example), then -``worker_id`` will return ``"master"``. - -Worker processes also have the following environment variables -defined: - -* ``PYTEST_XDIST_WORKER``: the name of the worker, e.g., ``"gw2"``. -* ``PYTEST_XDIST_WORKER_COUNT``: the total number of workers in this session, - e.g., ``"4"`` when ``-n 4`` is given in the command-line. - -The information about the worker_id in a test is stored in the ``TestReport`` as -well, under the ``worker_id`` attribute. - -Since version 2.0, the following functions are also available in the ``xdist`` module: - -.. code-block:: python - - def is_xdist_worker(request_or_session) -> bool: - """Return `True` if this is an xdist worker, `False` otherwise - - :param request_or_session: the `pytest` `request` or `session` object - """ - - def is_xdist_controller(request_or_session) -> bool: - """Return `True` if this is the xdist controller, `False` otherwise - - Note: this method also returns `False` when distribution has not been - activated at all. - - :param request_or_session: the `pytest` `request` or `session` object - """ - - def is_xdist_master(request_or_session) -> bool: - """Deprecated alias for is_xdist_controller.""" - - def get_xdist_worker_id(request_or_session) -> str: - """Return the id of the current worker ('gw0', 'gw1', etc) or 'master' - if running on the controller node. - - If not distributing tests (for example passing `-n0` or not passing `-n` at all) - also return 'master'. - - :param request_or_session: the `pytest` `request` or `session` object - """ - - -Identifying workers from the system environment -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -*New in version 2.4* - -If the `setproctitle`_ package is installed, ``pytest-xdist`` will use it to -update the process title (command line) on its workers to show their current -state. The titles used are ``[pytest-xdist running] file.py/node::id`` and -``[pytest-xdist idle]``, visible in standard tools like ``ps`` and ``top`` on -Linux, Mac OS X and BSD systems. For Windows, please follow `setproctitle`_'s -pointer regarding the Process Explorer tool. - -This is intended purely as an UX enhancement, e.g. to track down issues with -long-running or CPU intensive tests. Errors in changing the title are ignored -silently. Please try not to rely on the title format or title changes in -external scripts. - -.. _`setproctitle`: https://pypi.org/project/setproctitle/ - - -Uniquely identifying the current test run -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -*New in version 1.32.* - -If you need to globally distinguish one test run from others in your -workers, you can use the ``testrun_uid`` fixture. For instance, let's say you -wanted to create a separate database for each test run: - -.. code-block:: python - - import pytest - from posix_ipc import Semaphore, O_CREAT - - @pytest.fixture(scope="session", autouse=True) - def create_unique_database(testrun_uid): - """ create a unique database for this particular test run """ - database_url = f"psql://myapp-{testrun_uid}" - - with Semaphore(f"/{testrun_uid}-lock", flags=O_CREAT, initial_value=1): - if not database_exists(database_url): - create_database(database_url) - - @pytest.fixture() - def db(testrun_uid): - """ retrieve unique database """ - database_url = f"psql://myapp-{testrun_uid}" - return database_get_instance(database_url) - - -Additionally, during a test run, the following environment variable is defined: - -* ``PYTEST_XDIST_TESTRUNUID``: the unique id of the test run. - -Accessing ``sys.argv`` from the controller node in workers -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -To access the ``sys.argv`` passed to the command-line of the controller node, use -``request.config.workerinput["mainargv"]``. - - -Specifying test exec environments in an ini file -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -You can use pytest's ini file configuration to avoid typing common options. -You can for example make running with three subprocesses your default like this: - -.. code-block:: ini - - [pytest] - addopts = -n3 - -You can also add default environments like this: - -.. code-block:: ini - - [pytest] - addopts = --tx ssh=myhost//python=python3.9 --tx ssh=myhost//python=python3.6 - -and then just type:: - - pytest --dist=each - -to run tests in each of the environments. - - -Specifying "rsync" dirs in an ini-file -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -In a ``tox.ini`` or ``setup.cfg`` file in your root project directory -you may specify directories to include or to exclude in synchronisation: - -.. code-block:: ini - - [pytest] - rsyncdirs = . mypkg helperpkg - rsyncignore = .hg - -These directory specifications are relative to the directory -where the configuration file was found. - -.. _`pytest-xdist`: http://pypi.python.org/pypi/pytest-xdist -.. _`pytest-xdist repository`: https://github.com/pytest-dev/pytest-xdist -.. _`pytest`: http://pytest.org - - -Making session-scoped fixtures execute only once -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -``pytest-xdist`` is designed so that each worker process will perform its own collection and execute -a subset of all tests. This means that tests in different processes requesting a high-level -scoped fixture (for example ``session``) will execute the fixture code more than once, which -breaks expectations and might be undesired in certain situations. - -While ``pytest-xdist`` does not have a builtin support for ensuring a session-scoped fixture is -executed exactly once, this can be achieved by using a lock file for inter-process communication. - -The example below needs to execute the fixture ``session_data`` only once (because it is -resource intensive, or needs to execute only once to define configuration options, etc), so it makes -use of a `FileLock `_ to produce the fixture data only once -when the first process requests the fixture, while the other processes will then read -the data from a file. - -Here is the code: - -.. code-block:: python - - import json - - import pytest - from filelock import FileLock - - - @pytest.fixture(scope="session") - def session_data(tmp_path_factory, worker_id): - if worker_id == "master": - # not executing in with multiple workers, just produce the data and let - # pytest's fixture caching do its job - return produce_expensive_data() - - # get the temp directory shared by all workers - root_tmp_dir = tmp_path_factory.getbasetemp().parent - - fn = root_tmp_dir / "data.json" - with FileLock(str(fn) + ".lock"): - if fn.is_file(): - data = json.loads(fn.read_text()) - else: - data = produce_expensive_data() - fn.write_text(json.dumps(data)) - return data - - -The example above can also be use in cases a fixture needs to execute exactly once per test session, like -initializing a database service and populating initial tables. - -This technique might not work for every case, but should be a starting point for many situations -where executing a high-scope fixture exactly once is important. - - -How does xdist work? --------------------- - -``xdist`` works by spawning one or more **workers**, which are -controlled by the **controller**. Each **worker** is responsible for -performing a full test collection and afterwards running tests as -dictated by the **controller**. - -The execution flow is: - -1. **controller** spawns one or more **workers** at the beginning of the - test session. The communication between **controller** and **worker** - nodes makes use of `execnet `__ and - its - `gateways `__. - The actual interpreters executing the code for the **workers** might - be remote or local. - -2. Each **worker** itself is a mini pytest runner. **workers** at this - point perform a full test collection, sending back the collected - test-ids back to the **controller** which does not perform any - collection itself. - -3. The **controller** receives the result of the collection from all - nodes. At this point the **controller** performs some sanity check to - ensure that all **workers** collected the same tests (including - order), bailing out otherwise. If all is well, it converts the list - of test-ids into a list of simple indexes, where each index - corresponds to the position of that test in the original collection - list. This works because all nodes have the same collection list, and - saves bandwidth because the **controller** can now tell one of the - workers to just *execute test index 3* index of passing the full test - id. - -4. If **dist-mode** is **each**: the **controller** just sends the full - list of test indexes to each node at this moment. - -5. If **dist-mode** is **load**: the **controller** takes around 25% of - the tests and sends them one by one to each **worker** in a round - robin fashion. The rest of the tests will be distributed later as - **workers** finish tests (see below). - -6. Note that ``pytest_xdist_make_scheduler`` hook can be used to - implement custom tests distribution logic. - -7. **workers** re-implement ``pytest_runtestloop``: pytest’s default - implementation basically loops over all collected items in the - ``session`` object and executes the ``pytest_runtest_protocol`` for - each test item, but in xdist **workers** sit idly waiting for - **controller** to send tests for execution. As tests are received by - **workers**, ``pytest_runtest_protocol`` is executed for each test. - Here it worth noting an implementation detail: **workers** always - must keep at least one test item on their queue due to how the - ``pytest_runtest_protocol(item, nextitem)`` hook is defined: in order - to pass the ``nextitem`` to the hook, the worker must wait for more - instructions from controller before executing that remaining test. If - it receives more tests, then it can safely call - ``pytest_runtest_protocol`` because it knows what the ``nextitem`` - parameter will be. If it receives a “shutdown” signal, then it can - execute the hook passing ``nextitem`` as ``None``. - -8. As tests are started and completed at the **workers**, the results - are sent back to the **controller**, which then just forwards the - results to the appropriate pytest hooks: ``pytest_runtest_logstart`` - and ``pytest_runtest_logreport``. This way other plugins (for example - ``junitxml``) can work normally. The **controller** (when in - dist-mode **load**) decides to send more tests to a node when a test - completes, using some heuristics such as test durations and how many - tests each **worker** still has to run. - -9. When the **controller** has no more pending tests it will send a - “shutdown” signal to all **workers**, which will then run their - remaining tests to completion and shut down. At this point the - **controller** will sit waiting for **workers** to shut down, still - processing events such as ``pytest_runtest_logreport``. - -FAQ ---- - -**Question**: Why does each worker do its own collection, as opposed to having the -controller collect once and distribute from that collection to the -workers? - -If collection was performed by controller then it would have to -serialize collected items to send them through the wire, as workers live -in another process. The problem is that test items are not easily -(impossible?) to serialize, as they contain references to the test -functions, fixture managers, config objects, etc. Even if one manages to -serialize it, it seems it would be very hard to get it right and easy to -break by any small change in pytest. +Documentation is available at `Read The Docs `__. diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 00000000..69fa449d --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1 @@ +_build/ diff --git a/docs/boxed.rst b/docs/boxed.rst new file mode 100644 index 00000000..ec02bd57 --- /dev/null +++ b/docs/boxed.rst @@ -0,0 +1,9 @@ + +.. _boxed: + +Running tests in a boxed subprocess (moved to pytest-forked) +============================================================ + +This functionality has been moved to the +`pytest-forked `_ plugin, but the ``--boxed`` option +is still kept for backward compatibility. diff --git a/docs/changelog.rst b/docs/changelog.rst new file mode 100644 index 00000000..4c32ed85 --- /dev/null +++ b/docs/changelog.rst @@ -0,0 +1,5 @@ +========= +Changelog +========= + +.. include:: ../CHANGELOG.rst diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 00000000..ae24f632 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,54 @@ +# Configuration file for the Sphinx documentation builder. +# +# This file only contains a selection of the most common options. For a full +# list see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Path setup -------------------------------------------------------------- + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +# import os +# import sys +# sys.path.insert(0, os.path.abspath('.')) + + +# -- Project information ----------------------------------------------------- + +project = "pytest-xdist" +copyright = "2022, holger krekel and contributors" +author = "holger krekel and contributors" + +master_doc = "index" + +# -- General configuration --------------------------------------------------- + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + "sphinx_rtd_theme", +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ["_templates"] + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] + + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = "sphinx_rtd_theme" + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +# html_static_path = ['_static'] diff --git a/docs/crash.rst b/docs/crash.rst new file mode 100644 index 00000000..30cb11b5 --- /dev/null +++ b/docs/crash.rst @@ -0,0 +1,7 @@ +When tests crash +================ + +If a test crashes a worker, pytest-xdist will automatically restart that worker +and report the test’s failure. You can use the ``--max-worker-restart`` option +to limit the number of worker restarts that are allowed, or disable restarting +altogether using ``--max-worker-restart 0``. diff --git a/docs/distribution.rst b/docs/distribution.rst new file mode 100644 index 00000000..ae3002cf --- /dev/null +++ b/docs/distribution.rst @@ -0,0 +1,56 @@ +.. _parallelization: + +Running tests across multiple CPUs +================================== + +To send tests to multiple CPUs, use the ``-n`` (or ``--numprocesses``) option:: + + pytest -n 8 + +Pass ``-n auto`` to use as many processes as your computer has CPU cores. This +can lead to considerable speed ups, especially if your test suite takes a +noticeable amount of time. + +* ``--maxprocesses=maxprocesses``: limit the maximum number of workers to + process the tests. + +* ``--max-worker-restart``: maximum number of workers that can be restarted + when crashed (set to zero to disable this feature). + +The test distribution algorithm is configured with the ``--dist`` command-line option: + +.. _distribution modes: + +* ``--dist load`` **(default)**: Sends pending tests to any worker that is + available, without any guaranteed order. + +* ``--dist loadscope``: Tests are grouped by **module** for *test functions* + and by **class** for *test methods*. Groups are distributed to available + workers as whole units. This guarantees that all tests in a group run in the + same process. This can be useful if you have expensive module-level or + class-level fixtures. Grouping by class takes priority over grouping by + module. + +* ``--dist loadfile``: Tests are grouped by their containing file. Groups are + distributed to available workers as whole units. This guarantees that all + tests in a file run in the same worker. + +* ``--dist loadgroup``: Tests are grouped by the ``xdist_group`` mark. Groups are + distributed to available workers as whole units. This guarantees that all + tests with same ``xdist_group`` name run in the same worker. + + .. code-block:: python + + @pytest.mark.xdist_group(name="group1") + def test1(): + pass + + class TestA: + @pytest.mark.xdist_group("group1") + def test2(): + pass + + This will make sure ``test1`` and ``TestA::test2`` will run in the same worker. + Tests without the ``xdist_group`` mark are distributed normally as in the ``--dist=load`` mode. + +* ``--dist no``: The normal pytest execution mode, runs one test at a time (no distribution at all). diff --git a/docs/how-it-works.rst b/docs/how-it-works.rst new file mode 100644 index 00000000..3111d070 --- /dev/null +++ b/docs/how-it-works.rst @@ -0,0 +1,90 @@ +How it works? +============= + +``xdist`` works by spawning one or more **workers**, which are +controlled by the **controller**. Each **worker** is responsible for +performing a full test collection and afterwards running tests as +dictated by the **controller**. + +The execution flow is: + +1. **controller** spawns one or more **workers** at the beginning of the + test session. The communication between **controller** and **worker** + nodes makes use of `execnet `__ and + its + `gateways `__. + The actual interpreters executing the code for the **workers** might + be remote or local. + +2. Each **worker** itself is a mini pytest runner. **workers** at this + point perform a full test collection, sending back the collected + test-ids back to the **controller** which does not perform any + collection itself. + +3. The **controller** receives the result of the collection from all + nodes. At this point the **controller** performs some sanity check to + ensure that all **workers** collected the same tests (including + order), bailing out otherwise. If all is well, it converts the list + of test-ids into a list of simple indexes, where each index + corresponds to the position of that test in the original collection + list. This works because all nodes have the same collection list, and + saves bandwidth because the **controller** can now tell one of the + workers to just *execute test index 3* index of passing the full test + id. + +4. If **dist-mode** is **each**: the **controller** just sends the full + list of test indexes to each node at this moment. + +5. If **dist-mode** is **load**: the **controller** takes around 25% of + the tests and sends them one by one to each **worker** in a round + robin fashion. The rest of the tests will be distributed later as + **workers** finish tests (see below). + +6. Note that ``pytest_xdist_make_scheduler`` hook can be used to + implement custom tests distribution logic. + +7. **workers** re-implement ``pytest_runtestloop``: pytest’s default + implementation basically loops over all collected items in the + ``session`` object and executes the ``pytest_runtest_protocol`` for + each test item, but in xdist **workers** sit idly waiting for + **controller** to send tests for execution. As tests are received by + **workers**, ``pytest_runtest_protocol`` is executed for each test. + Here it worth noting an implementation detail: **workers** always + must keep at least one test item on their queue due to how the + ``pytest_runtest_protocol(item, nextitem)`` hook is defined: in order + to pass the ``nextitem`` to the hook, the worker must wait for more + instructions from controller before executing that remaining test. If + it receives more tests, then it can safely call + ``pytest_runtest_protocol`` because it knows what the ``nextitem`` + parameter will be. If it receives a “shutdown” signal, then it can + execute the hook passing ``nextitem`` as ``None``. + +8. As tests are started and completed at the **workers**, the results + are sent back to the **controller**, which then just forwards the + results to the appropriate pytest hooks: ``pytest_runtest_logstart`` + and ``pytest_runtest_logreport``. This way other plugins (for example + ``junitxml``) can work normally. The **controller** (when in + dist-mode **load**) decides to send more tests to a node when a test + completes, using some heuristics such as test durations and how many + tests each **worker** still has to run. + +9. When the **controller** has no more pending tests it will send a + “shutdown” signal to all **workers**, which will then run their + remaining tests to completion and shut down. At this point the + **controller** will sit waiting for **workers** to shut down, still + processing events such as ``pytest_runtest_logreport``. + +FAQ +--- + +**Question**: Why does each worker do its own collection, as opposed to having the +controller collect once and distribute from that collection to the +workers? + +If collection was performed by controller then it would have to +serialize collected items to send them through the wire, as workers live +in another process. The problem is that test items are not easily +(impossible?) to serialize, as they contain references to the test +functions, fixture managers, config objects, etc. Even if one manages to +serialize it, it seems it would be very hard to get it right and easy to +break by any small change in pytest. diff --git a/docs/how-to.rst b/docs/how-to.rst new file mode 100644 index 00000000..c37a3c1c --- /dev/null +++ b/docs/how-to.rst @@ -0,0 +1,224 @@ +How-tos +------- + +This section show cases how to accomplish some specialized tasks with ``pytest-xdist``. + +Identifying the worker process during a test +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +*New in version 1.15.* + +If you need to determine the identity of a worker process in +a test or fixture, you may use the ``worker_id`` fixture to do so: + +.. code-block:: python + + @pytest.fixture() + def user_account(worker_id): + """ use a different account in each xdist worker """ + return "account_%s" % worker_id + +When ``xdist`` is disabled (running with ``-n0`` for example), then +``worker_id`` will return ``"master"``. + +Worker processes also have the following environment variables +defined: + +* ``PYTEST_XDIST_WORKER``: the name of the worker, e.g., ``"gw2"``. +* ``PYTEST_XDIST_WORKER_COUNT``: the total number of workers in this session, + e.g., ``"4"`` when ``-n 4`` is given in the command-line. + +The information about the worker_id in a test is stored in the ``TestReport`` as +well, under the ``worker_id`` attribute. + +Since version 2.0, the following functions are also available in the ``xdist`` module: + +.. code-block:: python + + def is_xdist_worker(request_or_session) -> bool: + """Return `True` if this is an xdist worker, `False` otherwise + + :param request_or_session: the `pytest` `request` or `session` object + """ + + def is_xdist_controller(request_or_session) -> bool: + """Return `True` if this is the xdist controller, `False` otherwise + + Note: this method also returns `False` when distribution has not been + activated at all. + + :param request_or_session: the `pytest` `request` or `session` object + """ + + def is_xdist_master(request_or_session) -> bool: + """Deprecated alias for is_xdist_controller.""" + + def get_xdist_worker_id(request_or_session) -> str: + """Return the id of the current worker ('gw0', 'gw1', etc) or 'master' + if running on the controller node. + + If not distributing tests (for example passing `-n0` or not passing `-n` at all) + also return 'master'. + + :param request_or_session: the `pytest` `request` or `session` object + """ + + +Identifying workers from the system environment +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +*New in version 2.4* + +If the `setproctitle`_ package is installed, ``pytest-xdist`` will use it to +update the process title (command line) on its workers to show their current +state. The titles used are ``[pytest-xdist running] file.py/node::id`` and +``[pytest-xdist idle]``, visible in standard tools like ``ps`` and ``top`` on +Linux, Mac OS X and BSD systems. For Windows, please follow `setproctitle`_'s +pointer regarding the Process Explorer tool. + +This is intended purely as an UX enhancement, e.g. to track down issues with +long-running or CPU intensive tests. Errors in changing the title are ignored +silently. Please try not to rely on the title format or title changes in +external scripts. + +.. _`setproctitle`: https://pypi.org/project/setproctitle/ + + +Uniquely identifying the current test run +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +*New in version 1.32.* + +If you need to globally distinguish one test run from others in your +workers, you can use the ``testrun_uid`` fixture. For instance, let's say you +wanted to create a separate database for each test run: + +.. code-block:: python + + import pytest + from posix_ipc import Semaphore, O_CREAT + + @pytest.fixture(scope="session", autouse=True) + def create_unique_database(testrun_uid): + """ create a unique database for this particular test run """ + database_url = f"psql://myapp-{testrun_uid}" + + with Semaphore(f"/{testrun_uid}-lock", flags=O_CREAT, initial_value=1): + if not database_exists(database_url): + create_database(database_url) + + @pytest.fixture() + def db(testrun_uid): + """ retrieve unique database """ + database_url = f"psql://myapp-{testrun_uid}" + return database_get_instance(database_url) + + +Additionally, during a test run, the following environment variable is defined: + +* ``PYTEST_XDIST_TESTRUNUID``: the unique id of the test run. + +Accessing ``sys.argv`` from the controller node in workers +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +To access the ``sys.argv`` passed to the command-line of the controller node, use +``request.config.workerinput["mainargv"]``. + + +Specifying test exec environments in an ini file +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +You can use pytest's ini file configuration to avoid typing common options. +You can for example make running with three subprocesses your default like this: + +.. code-block:: ini + + [pytest] + addopts = -n3 + +You can also add default environments like this: + +.. code-block:: ini + + [pytest] + addopts = --tx ssh=myhost//python=python3.9 --tx ssh=myhost//python=python3.6 + +and then just type:: + + pytest --dist=each + +to run tests in each of the environments. + + +Specifying "rsync" dirs in an ini-file +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +In a ``tox.ini`` or ``setup.cfg`` file in your root project directory +you may specify directories to include or to exclude in synchronisation: + +.. code-block:: ini + + [pytest] + rsyncdirs = . mypkg helperpkg + rsyncignore = .hg + +These directory specifications are relative to the directory +where the configuration file was found. + +.. _`pytest-xdist`: http://pypi.python.org/pypi/pytest-xdist +.. _`pytest-xdist repository`: https://github.com/pytest-dev/pytest-xdist +.. _`pytest`: http://pytest.org + + +Making session-scoped fixtures execute only once +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +``pytest-xdist`` is designed so that each worker process will perform its own collection and execute +a subset of all tests. This means that tests in different processes requesting a high-level +scoped fixture (for example ``session``) will execute the fixture code more than once, which +breaks expectations and might be undesired in certain situations. + +While ``pytest-xdist`` does not have a builtin support for ensuring a session-scoped fixture is +executed exactly once, this can be achieved by using a lock file for inter-process communication. + +The example below needs to execute the fixture ``session_data`` only once (because it is +resource intensive, or needs to execute only once to define configuration options, etc), so it makes +use of a `FileLock `_ to produce the fixture data only once +when the first process requests the fixture, while the other processes will then read +the data from a file. + +Here is the code: + +.. code-block:: python + + import json + + import pytest + from filelock import FileLock + + + @pytest.fixture(scope="session") + def session_data(tmp_path_factory, worker_id): + if worker_id == "master": + # not executing in with multiple workers, just produce the data and let + # pytest's fixture caching do its job + return produce_expensive_data() + + # get the temp directory shared by all workers + root_tmp_dir = tmp_path_factory.getbasetemp().parent + + fn = root_tmp_dir / "data.json" + with FileLock(str(fn) + ".lock"): + if fn.is_file(): + data = json.loads(fn.read_text()) + else: + data = produce_expensive_data() + fn.write_text(json.dumps(data)) + return data + + +The example above can also be use in cases a fixture needs to execute exactly once per test session, like +initializing a database service and populating initial tables. + +This technique might not work for every case, but should be a starting point for many situations +where executing a high-scope fixture exactly once is important. diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 00000000..94747b15 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,63 @@ +pytest-xdist +============ + +The `pytest-xdist`_ plugin extends pytest with new test execution modes, the most used being distributing +tests across multiple CPUs to speed up test execution:: + + pytest -n auto + +With this call, pytest will spawn a number of workers processes equal to the number of available CPUs, and distribute +the tests randomly across them. + +.. note:: + Due to how pytest-xdist is implemented, the ``-s/--capture=no`` option does not work. + + +Installation +------------ + +Install the plugin with:: + + pip install pytest-xdist + + +To use ``psutil`` for detection of the number of CPUs available, install the ``psutil`` extra:: + + pip install pytest-xdist[psutil] + +Features +-------- + +* Test run :ref:`parallelization`: tests can be executed across multiple CPUs or hosts. + This allows to speed up development or to use special resources of :ref:`remote machines`. + +* ``--looponfail``: run your tests repeatedly in a subprocess. After each run + pytest waits until a file in your project changes and then re-runs + the previously failing tests. This is repeated until all tests pass + after which again a full run is performed. + +* :ref:`Multi-Platform` coverage: you can specify different Python interpreters + or different platforms and run tests in parallel on all of them. + + Before running tests remotely, ``pytest`` efficiently "rsyncs" your + program source code to the remote place. + You may specify different Python versions and interpreters. It does not + installs/synchronize dependencies however. + + **Note**: this mode exists mostly for backward compatibility, as modern development + relies on continuous integration for multi-platform testing. + + + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + distribution + subprocess + boxed + remote + crash + how-to + how-it-works + changelog diff --git a/docs/remote.rst b/docs/remote.rst new file mode 100644 index 00000000..7ffab180 --- /dev/null +++ b/docs/remote.rst @@ -0,0 +1,72 @@ + +.. _`Multi-Platform`: +.. _`remote machines`: + +Sending tests to remote SSH accounts +==================================== + +Suppose you have a package ``mypkg`` which contains some +tests that you can successfully run locally. And you +have a ssh-reachable machine ``myhost``. Then +you can ad-hoc distribute your tests by typing:: + + pytest -d --tx ssh=myhostpopen --rsyncdir mypkg mypkg + +This will synchronize your :code:`mypkg` package directory +to a remote ssh account and then locally collect tests +and send them to remote places for execution. + +You can specify multiple :code:`--rsyncdir` directories +to be sent to the remote side. + +.. note:: + + For pytest to collect and send tests correctly + you not only need to make sure all code and tests + directories are rsynced, but that any test (sub) directory + also has an :code:`__init__.py` file because internally + pytest references tests as a fully qualified python + module path. **You will otherwise get strange errors** + during setup of the remote side. + + +You can specify multiple :code:`--rsyncignore` glob patterns +to be ignored when file are sent to the remote side. +There are also internal ignores: :code:`.*, *.pyc, *.pyo, *~` +Those you cannot override using rsyncignore command-line or +ini-file option(s). + + +Sending tests to remote Socket Servers +-------------------------------------- + +Download the single-module `socketserver.py`_ Python program +and run it like this:: + + python socketserver.py + +It will tell you that it starts listening on the default +port. You can now on your home machine specify this +new socket host with something like this:: + + pytest -d --tx socket=192.168.1.102:8888 --rsyncdir mypkg mypkg + + + +Running tests on many platforms at once +--------------------------------------- + +The basic command to run tests on multiple platforms is:: + + pytest --dist=each --tx=spec1 --tx=spec2 + +If you specify a windows host, an OSX host and a Linux +environment this command will send each tests to all +platforms - and report back failures from all platforms +at once. The specifications strings use the `xspec syntax`_. + +.. _`xspec syntax`: https://codespeak.net/execnet/basics.html#xspec + +.. _`execnet`: https://codespeak.net/execnet + +.. _`socketserver.py`: https://raw.githubusercontent.com/pytest-dev/execnet/master/execnet/script/socketserver.py diff --git a/docs/subprocess.rst b/docs/subprocess.rst new file mode 100644 index 00000000..2148a867 --- /dev/null +++ b/docs/subprocess.rst @@ -0,0 +1,16 @@ +Running tests in a Python subprocess +==================================== + +To instantiate a ``python3.9`` subprocess and send tests to it, you may type:: + + pytest -d --tx popen//python=python3.9 + +This will start a subprocess which is run with the ``python3.9`` +Python interpreter, found in your system binary lookup path. + +If you prefix the --tx option value like this:: + + --tx 3*popen//python=python3.9 + +then three subprocesses would be created and tests +will be load-balanced across these three processes. diff --git a/setup.cfg b/setup.cfg index ce97d810..03e4a918 100644 --- a/setup.cfg +++ b/setup.cfg @@ -30,6 +30,11 @@ classifiers = Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 license_file = LICENSE +project_urls = + Documentation=https://pytest-xdist.readthedocs.io/en/latest + Changelog=https://pytest-xdist.readthedocs.io/en/latest/changelog.html + Source=https://github.com/pytest-dev/pytest-xdist + Tracker=https://github.com/pytest-dev/pytest-xdist/issues [options] packages = find: diff --git a/tox.ini b/tox.ini index 3ab7109d..2f79f3e7 100644 --- a/tox.ini +++ b/tox.ini @@ -49,6 +49,15 @@ deps = commands = towncrier --version {posargs} --yes +[testenv:docs] +basepython = python3 +usedevelop = True +deps = + sphinx + sphinx_rtd_theme +commands = + sphinx-build -W --keep-going -b html docs docs/_build/html {posargs:} + [pytest] # pytest-services also defines a worker_id fixture, disable # it so they don't conflict with each other (#611). From 290b322a5d48290397ad698fc1dcb729cbe62e07 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 1 Feb 2022 08:34:13 -0300 Subject: [PATCH 092/113] [pre-commit.ci] pre-commit autoupdate (#755) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- testing/acceptance_test.py | 96 +++++++++++++------------------------- 2 files changed, 33 insertions(+), 65 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 223427dc..8a35e525 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/psf/black - rev: 21.12b0 + rev: 22.1.0 hooks: - id: black args: [--safe, --quiet, --target-version, py35] diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index 44443d03..3407272e 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -1244,22 +1244,14 @@ def test(self, i): "test_b.py::TestB", result.outlines ) - assert ( - test_a_workers_and_test_count - in ( - {"gw0": 10}, - {"gw1": 0}, - ) - or test_a_workers_and_test_count in ({"gw0": 0}, {"gw1": 10}) - ) - assert ( - test_b_workers_and_test_count - in ( - {"gw0": 10}, - {"gw1": 0}, - ) - or test_b_workers_and_test_count in ({"gw0": 0}, {"gw1": 10}) - ) + assert test_a_workers_and_test_count in ( + {"gw0": 10}, + {"gw1": 0}, + ) or test_a_workers_and_test_count in ({"gw0": 0}, {"gw1": 10}) + assert test_b_workers_and_test_count in ( + {"gw0": 10}, + {"gw1": 0}, + ) or test_b_workers_and_test_count in ({"gw0": 0}, {"gw1": 10}) def test_by_class(self, pytester: pytest.Pytester) -> None: pytester.makepyfile( @@ -1284,22 +1276,14 @@ def test(self, i): "test_a.py::TestB", result.outlines ) - assert ( - test_a_workers_and_test_count - in ( - {"gw0": 10}, - {"gw1": 0}, - ) - or test_a_workers_and_test_count in ({"gw0": 0}, {"gw1": 10}) - ) - assert ( - test_b_workers_and_test_count - in ( - {"gw0": 10}, - {"gw1": 0}, - ) - or test_b_workers_and_test_count in ({"gw0": 0}, {"gw1": 10}) - ) + assert test_a_workers_and_test_count in ( + {"gw0": 10}, + {"gw1": 0}, + ) or test_a_workers_and_test_count in ({"gw0": 0}, {"gw1": 10}) + assert test_b_workers_and_test_count in ( + {"gw0": 10}, + {"gw1": 0}, + ) or test_b_workers_and_test_count in ({"gw0": 0}, {"gw1": 10}) def test_module_single_start(self, pytester: pytest.Pytester) -> None: """Fix test suite never finishing in case all workers start with a single test (#277).""" @@ -1346,22 +1330,14 @@ def test(self, i): "test_b.py::TestA", result.outlines ) - assert ( - test_a_workers_and_test_count - in ( - {"gw0": 5}, - {"gw1": 0}, - ) - or test_a_workers_and_test_count in ({"gw0": 0}, {"gw1": 5}) - ) - assert ( - test_b_workers_and_test_count - in ( - {"gw0": 5}, - {"gw1": 0}, - ) - or test_b_workers_and_test_count in ({"gw0": 0}, {"gw1": 5}) - ) + assert test_a_workers_and_test_count in ( + {"gw0": 5}, + {"gw1": 0}, + ) or test_a_workers_and_test_count in ({"gw0": 0}, {"gw1": 5}) + assert test_b_workers_and_test_count in ( + {"gw0": 5}, + {"gw1": 0}, + ) or test_b_workers_and_test_count in ({"gw0": 0}, {"gw1": 5}) assert ( test_a_workers_and_test_count.items() == test_b_workers_and_test_count.items() @@ -1391,22 +1367,14 @@ def test(self, i): "test_a.py::TestB", result.outlines ) - assert ( - test_a_workers_and_test_count - in ( - {"gw0": 10}, - {"gw1": 0}, - ) - or test_a_workers_and_test_count in ({"gw0": 0}, {"gw1": 10}) - ) - assert ( - test_b_workers_and_test_count - in ( - {"gw0": 10}, - {"gw1": 0}, - ) - or test_b_workers_and_test_count in ({"gw0": 0}, {"gw1": 10}) - ) + assert test_a_workers_and_test_count in ( + {"gw0": 10}, + {"gw1": 0}, + ) or test_a_workers_and_test_count in ({"gw0": 0}, {"gw1": 10}) + assert test_b_workers_and_test_count in ( + {"gw0": 10}, + {"gw1": 0}, + ) or test_b_workers_and_test_count in ({"gw0": 0}, {"gw1": 10}) assert ( test_a_workers_and_test_count.items() == test_b_workers_and_test_count.items() From d79ef915a2721a64dbb966811909a635841694da Mon Sep 17 00:00:00 2001 From: Kian-Meng Ang Date: Mon, 7 Feb 2022 21:59:38 +0800 Subject: [PATCH 093/113] Fix typos --- CHANGELOG.rst | 2 +- src/xdist/plugin.py | 2 +- src/xdist/scheduler/loadscope.py | 4 ++-- tox.ini | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index c20908c2..d218ba55 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -4,7 +4,7 @@ pytest-xdist 2.5.0 (2021-12-10) Deprecations and Removals ------------------------- -- `#468 `_: The ``--boxed`` commmand line argument is deprecated. Install pytest-forked and use ``--forked`` instead. pytest-xdist 3.0.0 will remove the ``--boxed`` argument and pytest-forked dependency. +- `#468 `_: The ``--boxed`` command line argument is deprecated. Install pytest-forked and use ``--forked`` instead. pytest-xdist 3.0.0 will remove the ``--boxed`` argument and pytest-forked dependency. Features diff --git a/src/xdist/plugin.py b/src/xdist/plugin.py index 85f76e82..d0448fa7 100644 --- a/src/xdist/plugin.py +++ b/src/xdist/plugin.py @@ -198,7 +198,7 @@ def pytest_configure(config): tr.showfspath = False if config.getoption("boxed"): warning = DeprecationWarning( - "The --boxed commmand line argument is deprecated. " + "The --boxed command line argument is deprecated. " "Install pytest-forked and use --forked instead. " "pytest-xdist 3.0.0 will remove the --boxed argument and pytest-forked dependency." ) diff --git a/src/xdist/scheduler/loadscope.py b/src/xdist/scheduler/loadscope.py index c25e4769..69d9d9a2 100644 --- a/src/xdist/scheduler/loadscope.py +++ b/src/xdist/scheduler/loadscope.py @@ -361,12 +361,12 @@ def schedule(self): extra_nodes = len(self.nodes) - len(self.workqueue) if extra_nodes > 0: - self.log("Shuting down {} nodes".format(extra_nodes)) + self.log("Shutting down {} nodes".format(extra_nodes)) for _ in range(extra_nodes): unused_node, assigned = self.assigned_work.popitem(last=True) - self.log("Shuting down unused node {}".format(unused_node)) + self.log("Shutting down unused node {}".format(unused_node)) unused_node.shutdown() # Assign initial workload diff --git a/tox.ini b/tox.ini index 2f79f3e7..2578ae57 100644 --- a/tox.ini +++ b/tox.ini @@ -39,7 +39,7 @@ commands = pre-commit run --all-files --show-diff-on-failure [testenv:release] changedir= -decription = do a release, required posarg of the version number +description = do a release, required posarg of the version number basepython = python3.7 skipsdist = True usedevelop = True From 14e1d80f1095627d6ef894474c1eb3a148a2a1cb Mon Sep 17 00:00:00 2001 From: Marco Sanchotene Date: Tue, 8 Mar 2022 19:11:48 -0300 Subject: [PATCH 094/113] Add instructions to create one file for each worker --- docs/how-to.rst | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/docs/how-to.rst b/docs/how-to.rst index c37a3c1c..26445d95 100644 --- a/docs/how-to.rst +++ b/docs/how-to.rst @@ -222,3 +222,52 @@ initializing a database service and populating initial tables. This technique might not work for every case, but should be a starting point for many situations where executing a high-scope fixture exactly once is important. + + +Creating one log file for each worker +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +To create one log file for each worker with ``pytest-xdist``, add +an option to ``pytest.ini`` for the file base name. Then, in ``conftest.py``, +register it with ``pytest_addoption(parser)`` and use ``pytest_configure(config)`` +to rename it with the worker id. + +Example: + +.. code-block:: python + + # content of pytest.ini + [pytest] + log_file_format = %(asctime)s %(name)s %(levelname)s %(message)s + log_file_level = INFO + worker_log_file = tests_%w.log + + +.. code-block:: python + + # content of conftest.py + def pytest_addoption(parser): + log_help_text = 'Similar to log_file, but %w will be replaced with a worker identifier.' + parser.addini('worker_log_file', help=log_help_text) + + + def pytest_configure(config): + configure_logger(config) + + + def configure_logger(config): + if xdist_is_enabled(): + log_file = config.getini('worker_log_file') + logging.basicConfig( + format=config.getini('log_file_format'), + filename=log_file.replace('%w', os.environ.get('PYTEST_XDIST_WORKER')), + level=config.getini('log_file_level') + ) + + + def xdist_is_enabled(): + return os.environ.get('PYTEST_XDIST_WORKER') is not None + + +If running tests with ``-n3``, for example, three files would be created and named +as ``tests_gw0.log``, ``tests_gw1.log`` and ``tests_gw2.log``. From e3c1a3de672393bc868d1703cb0bf08329618e55 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 10 Mar 2022 09:11:00 -0300 Subject: [PATCH 095/113] Remove redundant type casts --- testing/test_looponfail.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/testing/test_looponfail.py b/testing/test_looponfail.py index cd1cc43e..c70b00f5 100644 --- a/testing/test_looponfail.py +++ b/testing/test_looponfail.py @@ -1,5 +1,3 @@ -from typing import cast - import py import pytest import shutil @@ -125,7 +123,7 @@ def test_failures_somewhere(self, pytester: pytest.Pytester) -> None: failures = control.runsession() assert failures control.setup() - item_path = item.path if PYTEST_GTE_7 else Path(cast(py.path.local, item.fspath)) # type: ignore[attr-defined] + item_path = item.path if PYTEST_GTE_7 else Path(str(item.fspath)) # type: ignore[attr-defined] item_path.write_text("def test_func():\n assert 1\n") removepyc(item_path) topdir, failures = control.runsession()[:2] @@ -146,7 +144,7 @@ def test_func(): if PYTEST_GTE_7: modcol_path = modcol.path # type:ignore[attr-defined] else: - modcol_path = Path(cast(py.path.local, modcol.fspath)) + modcol_path = Path(str(modcol.fspath)) modcol_path.write_text( textwrap.dedent( @@ -179,7 +177,7 @@ def test_func(): if PYTEST_GTE_7: parent = modcol.path.parent.parent # type: ignore[attr-defined] else: - parent = Path(cast(py.path.local, modcol.fspath).dirpath().dirpath()) + parent = Path(modcol.fspath.dirpath().dirpath()) monkeypatch.chdir(parent) modcol.config.args = [ str(Path(x).relative_to(parent)) for x in modcol.config.args From 797867f23ddfc471c01aea4461ebaf8a9f7eb1c9 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 10 Mar 2022 09:25:37 -0300 Subject: [PATCH 096/113] Simplify example and use syntax for env vars --- docs/how-to.rst | 41 +++++++++++++++++++---------------------- 1 file changed, 19 insertions(+), 22 deletions(-) diff --git a/docs/how-to.rst b/docs/how-to.rst index 26445d95..37f62bb9 100644 --- a/docs/how-to.rst +++ b/docs/how-to.rst @@ -24,9 +24,13 @@ When ``xdist`` is disabled (running with ``-n0`` for example), then Worker processes also have the following environment variables defined: -* ``PYTEST_XDIST_WORKER``: the name of the worker, e.g., ``"gw2"``. -* ``PYTEST_XDIST_WORKER_COUNT``: the total number of workers in this session, - e.g., ``"4"`` when ``-n 4`` is given in the command-line. +.. envvar:: PYTEST_XDIST_WORKER + +The name of the worker, e.g., ``"gw2"``. + +.. envvar:: PYTEST_XDIST_WORKER_COUNT + +The total number of workers in this session, e.g., ``"4"`` when ``-n 4`` is given in the command-line. The information about the worker_id in a test is stored in the ``TestReport`` as well, under the ``worker_id`` attribute. @@ -116,7 +120,9 @@ wanted to create a separate database for each test run: Additionally, during a test run, the following environment variable is defined: -* ``PYTEST_XDIST_TESTRUNUID``: the unique id of the test run. +.. envvar:: PYTEST_XDIST_TESTRUNUID + +The unique id of the test run. Accessing ``sys.argv`` from the controller node in workers ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -227,47 +233,38 @@ where executing a high-scope fixture exactly once is important. Creating one log file for each worker ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -To create one log file for each worker with ``pytest-xdist``, add +To create one log file for each worker with ``pytest-xdist``, you can leverage :envvar:`PYTEST_XDIST_WORKER` an option to ``pytest.ini`` for the file base name. Then, in ``conftest.py``, register it with ``pytest_addoption(parser)`` and use ``pytest_configure(config)`` to rename it with the worker id. Example: -.. code-block:: python +.. code-block:: ini - # content of pytest.ini [pytest] log_file_format = %(asctime)s %(name)s %(levelname)s %(message)s log_file_level = INFO - worker_log_file = tests_%w.log + worker_log_file = tests_{worker_id}.log .. code-block:: python # content of conftest.py def pytest_addoption(parser): - log_help_text = 'Similar to log_file, but %w will be replaced with a worker identifier.' - parser.addini('worker_log_file', help=log_help_text) + parser.addini('worker_log_file', help='Similar to log_file, but %w will be replaced with a worker identifier.') def pytest_configure(config): - configure_logger(config) - - - def configure_logger(config): - if xdist_is_enabled(): + worker_id = os.environ.get('PYTEST_XDIST_WORKER') + if worker_id is not None: log_file = config.getini('worker_log_file') logging.basicConfig( format=config.getini('log_file_format'), - filename=log_file.replace('%w', os.environ.get('PYTEST_XDIST_WORKER')), + filename=log_file.format(worker_id=worker_id), level=config.getini('log_file_level') ) - def xdist_is_enabled(): - return os.environ.get('PYTEST_XDIST_WORKER') is not None - - -If running tests with ``-n3``, for example, three files would be created and named -as ``tests_gw0.log``, ``tests_gw1.log`` and ``tests_gw2.log``. +When running the tests with ``-n3``, for example, three files will be created in the current directory: +``tests_gw0.log``, ``tests_gw1.log`` and ``tests_gw2.log``. From ad5468b6e76700c477938da9019823f9f870183b Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 10 Mar 2022 09:26:16 -0300 Subject: [PATCH 097/113] Add blacken docs pre-commit hook --- .pre-commit-config.yaml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8a35e525..dcc4c726 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,6 +4,11 @@ repos: hooks: - id: black args: [--safe, --quiet, --target-version, py35] +- repo: https://github.com/asottile/blacken-docs + rev: v1.12.0 + hooks: + - id: blacken-docs + additional_dependencies: [black==22.1.0] - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.1.0 hooks: From 7f04ce35c90955c035e74a74deba5085635bc155 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 10 Mar 2022 09:33:23 -0300 Subject: [PATCH 098/113] Add blacken-docs and use autodoc --- .pre-commit-config.yaml | 2 +- docs/conf.py | 1 + docs/distribution.rst | 1 + docs/how-to.rst | 48 ++++++++++++----------------------------- 4 files changed, 17 insertions(+), 35 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index dcc4c726..5e4eea23 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -8,7 +8,7 @@ repos: rev: v1.12.0 hooks: - id: blacken-docs - additional_dependencies: [black==22.1.0] + additional_dependencies: [black==20.8b1] - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.1.0 hooks: diff --git a/docs/conf.py b/docs/conf.py index ae24f632..ec93d438 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -30,6 +30,7 @@ # ones. extensions = [ "sphinx_rtd_theme", + "sphinx.ext.autodoc", ] # Add any paths that contain templates here, relative to this directory. diff --git a/docs/distribution.rst b/docs/distribution.rst index ae3002cf..5a867439 100644 --- a/docs/distribution.rst +++ b/docs/distribution.rst @@ -45,6 +45,7 @@ The test distribution algorithm is configured with the ``--dist`` command-line o def test1(): pass + class TestA: @pytest.mark.xdist_group("group1") def test2(): diff --git a/docs/how-to.rst b/docs/how-to.rst index 37f62bb9..268a9691 100644 --- a/docs/how-to.rst +++ b/docs/how-to.rst @@ -37,36 +37,11 @@ well, under the ``worker_id`` attribute. Since version 2.0, the following functions are also available in the ``xdist`` module: -.. code-block:: python - - def is_xdist_worker(request_or_session) -> bool: - """Return `True` if this is an xdist worker, `False` otherwise - - :param request_or_session: the `pytest` `request` or `session` object - """ - - def is_xdist_controller(request_or_session) -> bool: - """Return `True` if this is the xdist controller, `False` otherwise - - Note: this method also returns `False` when distribution has not been - activated at all. - - :param request_or_session: the `pytest` `request` or `session` object - """ - - def is_xdist_master(request_or_session) -> bool: - """Deprecated alias for is_xdist_controller.""" - - def get_xdist_worker_id(request_or_session) -> str: - """Return the id of the current worker ('gw0', 'gw1', etc) or 'master' - if running on the controller node. - - If not distributing tests (for example passing `-n0` or not passing `-n` at all) - also return 'master'. - - :param request_or_session: the `pytest` `request` or `session` object - """ +.. autofunction:: xdist.is_xdist_worker +.. autofunction:: xdist.is_xdist_controller +.. autofunction:: xdist.is_xdist_master +.. autofunction:: xdist.get_xdist_worker_id Identifying workers from the system environment ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -102,6 +77,7 @@ wanted to create a separate database for each test run: import pytest from posix_ipc import Semaphore, O_CREAT + @pytest.fixture(scope="session", autouse=True) def create_unique_database(testrun_uid): """ create a unique database for this particular test run """ @@ -111,6 +87,7 @@ wanted to create a separate database for each test run: if not database_exists(database_url): create_database(database_url) + @pytest.fixture() def db(testrun_uid): """ retrieve unique database """ @@ -252,17 +229,20 @@ Example: # content of conftest.py def pytest_addoption(parser): - parser.addini('worker_log_file', help='Similar to log_file, but %w will be replaced with a worker identifier.') + parser.addini( + "worker_log_file", + help="Similar to log_file, but %w will be replaced with a worker identifier.", + ) def pytest_configure(config): - worker_id = os.environ.get('PYTEST_XDIST_WORKER') + worker_id = os.environ.get("PYTEST_XDIST_WORKER") if worker_id is not None: - log_file = config.getini('worker_log_file') + log_file = config.getini("worker_log_file") logging.basicConfig( - format=config.getini('log_file_format'), + format=config.getini("log_file_format"), filename=log_file.format(worker_id=worker_id), - level=config.getini('log_file_level') + level=config.getini("log_file_level"), ) From 34f63600ee7a75aff0343eda1379b86950a66007 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 10 Mar 2022 11:51:43 -0300 Subject: [PATCH 099/113] Simplify the example about creating one log per worker No need to use a custom option as this complicates the example a bit and might distract from the main details. --- docs/how-to.rst | 21 ++------------------- 1 file changed, 2 insertions(+), 19 deletions(-) diff --git a/docs/how-to.rst b/docs/how-to.rst index 268a9691..c357ae52 100644 --- a/docs/how-to.rst +++ b/docs/how-to.rst @@ -211,37 +211,20 @@ Creating one log file for each worker ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ To create one log file for each worker with ``pytest-xdist``, you can leverage :envvar:`PYTEST_XDIST_WORKER` -an option to ``pytest.ini`` for the file base name. Then, in ``conftest.py``, -register it with ``pytest_addoption(parser)`` and use ``pytest_configure(config)`` -to rename it with the worker id. +to generate a unique filename for each worker. Example: -.. code-block:: ini - - [pytest] - log_file_format = %(asctime)s %(name)s %(levelname)s %(message)s - log_file_level = INFO - worker_log_file = tests_{worker_id}.log - - .. code-block:: python # content of conftest.py - def pytest_addoption(parser): - parser.addini( - "worker_log_file", - help="Similar to log_file, but %w will be replaced with a worker identifier.", - ) - - def pytest_configure(config): worker_id = os.environ.get("PYTEST_XDIST_WORKER") if worker_id is not None: log_file = config.getini("worker_log_file") logging.basicConfig( format=config.getini("log_file_format"), - filename=log_file.format(worker_id=worker_id), + filename=f"test_{worker_id}.log", level=config.getini("log_file_level"), ) From 41a4d439c830d20ba2a18f18ac8b938949cb628e Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 10 Mar 2022 12:24:32 -0300 Subject: [PATCH 100/113] Fix typo in docs --- docs/how-to.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/how-to.rst b/docs/how-to.rst index c357ae52..5d6ed128 100644 --- a/docs/how-to.rst +++ b/docs/how-to.rst @@ -224,7 +224,7 @@ Example: log_file = config.getini("worker_log_file") logging.basicConfig( format=config.getini("log_file_format"), - filename=f"test_{worker_id}.log", + filename=f"tests_{worker_id}.log", level=config.getini("log_file_level"), ) From a685a04bd150d3915a0fd594c4099024be175a1e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 14 Mar 2022 23:27:19 +0000 Subject: [PATCH 101/113] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/blacken-docs: v1.12.0 → v1.12.1](https://github.com/asottile/blacken-docs/compare/v1.12.0...v1.12.1) - [github.com/asottile/pyupgrade: v2.31.0 → v2.31.1](https://github.com/asottile/pyupgrade/compare/v2.31.0...v2.31.1) - [github.com/pre-commit/mirrors-mypy: v0.931 → v0.940](https://github.com/pre-commit/mirrors-mypy/compare/v0.931...v0.940) --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5e4eea23..ad3fd321 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -5,7 +5,7 @@ repos: - id: black args: [--safe, --quiet, --target-version, py35] - repo: https://github.com/asottile/blacken-docs - rev: v1.12.0 + rev: v1.12.1 hooks: - id: blacken-docs additional_dependencies: [black==20.8b1] @@ -21,7 +21,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/asottile/pyupgrade - rev: v2.31.0 + rev: v2.31.1 hooks: - id: pyupgrade args: [--py3-plus] @@ -34,7 +34,7 @@ repos: language: python additional_dependencies: [pygments, restructuredtext_lint] - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.931 + rev: v0.940 hooks: - id: mypy files: ^(src/|testing/) From 2fefa36db65a68df9cae56873ec22868133c3ecf Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 21 Mar 2022 23:42:02 +0000 Subject: [PATCH 102/113] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-mypy: v0.940 → v0.941](https://github.com/pre-commit/mirrors-mypy/compare/v0.940...v0.941) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ad3fd321..fc9a12d4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -34,7 +34,7 @@ repos: language: python additional_dependencies: [pygments, restructuredtext_lint] - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.940 + rev: v0.941 hooks: - id: mypy files: ^(src/|testing/) From 6fca8686f55e30d903abff63af8d0624cc9d2deb Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 28 Mar 2022 19:38:44 +0000 Subject: [PATCH 103/113] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-mypy: v0.941 → v0.942](https://github.com/pre-commit/mirrors-mypy/compare/v0.941...v0.942) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index fc9a12d4..b81e5222 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -34,7 +34,7 @@ repos: language: python additional_dependencies: [pygments, restructuredtext_lint] - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.941 + rev: v0.942 hooks: - id: mypy files: ^(src/|testing/) From eca7f202b66a66431472c69026dde57a88f55e99 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 4 Apr 2022 20:28:36 +0000 Subject: [PATCH 104/113] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/psf/black: 22.1.0 → 22.3.0](https://github.com/psf/black/compare/22.1.0...22.3.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b81e5222..7b313633 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/psf/black - rev: 22.1.0 + rev: 22.3.0 hooks: - id: black args: [--safe, --quiet, --target-version, py35] From 8723495e9b9e819718a03ac3f0104c9a376f2f62 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 11 Apr 2022 21:29:34 +0000 Subject: [PATCH 105/113] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/pre-commit-hooks: v4.1.0 → v4.2.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.1.0...v4.2.0) - [github.com/asottile/pyupgrade: v2.31.1 → v2.32.0](https://github.com/asottile/pyupgrade/compare/v2.31.1...v2.32.0) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7b313633..9e1fd25a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -10,7 +10,7 @@ repos: - id: blacken-docs additional_dependencies: [black==20.8b1] - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.1.0 + rev: v4.2.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer @@ -21,7 +21,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/asottile/pyupgrade - rev: v2.31.1 + rev: v2.32.0 hooks: - id: pyupgrade args: [--py3-plus] From 8652ef91e6d4d339f6a8af05460add2ae55b2b22 Mon Sep 17 00:00:00 2001 From: auziel <2196255+auziel@users.noreply.github.com> Date: Thu, 21 Apr 2022 00:00:28 +0300 Subject: [PATCH 106/113] Fix the example of using --rsyncdir option (#777) Co-authored-by: Ronny Pfannschmidt Co-authored-by: Bruno Oliveira --- docs/remote.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/remote.rst b/docs/remote.rst index 7ffab180..997bef37 100644 --- a/docs/remote.rst +++ b/docs/remote.rst @@ -10,7 +10,7 @@ tests that you can successfully run locally. And you have a ssh-reachable machine ``myhost``. Then you can ad-hoc distribute your tests by typing:: - pytest -d --tx ssh=myhostpopen --rsyncdir mypkg mypkg + pytest -d --rsyncdir mypkg --tx ssh=myhostpopen mypkg/tests/unit/test_something.py This will synchronize your :code:`mypkg` package directory to a remote ssh account and then locally collect tests @@ -49,7 +49,7 @@ It will tell you that it starts listening on the default port. You can now on your home machine specify this new socket host with something like this:: - pytest -d --tx socket=192.168.1.102:8888 --rsyncdir mypkg mypkg + pytest -d --tx socket=192.168.1.102:8888 --rsyncdir mypkg From 7743e14fd4a5a3bd088f82dec982b9013370ba50 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 4 May 2022 09:21:39 -0300 Subject: [PATCH 107/113] [pre-commit.ci] pre-commit autoupdate (#781) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9e1fd25a..77df3deb 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -34,7 +34,7 @@ repos: language: python additional_dependencies: [pygments, restructuredtext_lint] - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.942 + rev: v0.950 hooks: - id: mypy files: ^(src/|testing/) From 2220ccecb1d45b079dc9ac3cb6f90de4e1085ef2 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 9 May 2022 20:58:23 +0000 Subject: [PATCH 108/113] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v2.32.0 → v2.32.1](https://github.com/asottile/pyupgrade/compare/v2.32.0...v2.32.1) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 77df3deb..ba69470a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -21,7 +21,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/asottile/pyupgrade - rev: v2.32.0 + rev: v2.32.1 hooks: - id: pyupgrade args: [--py3-plus] From 44cbd7fdc58c4de684be8c46dc61e9e43dbdb4fb Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 30 May 2022 21:01:27 +0000 Subject: [PATCH 109/113] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-mypy: v0.950 → v0.960](https://github.com/pre-commit/mirrors-mypy/compare/v0.950...v0.960) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ba69470a..9c2836d9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -34,7 +34,7 @@ repos: language: python additional_dependencies: [pygments, restructuredtext_lint] - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.950 + rev: v0.960 hooks: - id: mypy files: ^(src/|testing/) From 9d69c1a7da7f6a62796cc60a76670a582bc993be Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Tue, 12 Jul 2022 16:54:51 +0200 Subject: [PATCH 110/113] correct setuptools_scm support modern setuptools_scm drops python 3.6 * use build isolation * drop setup_requires --- setup.cfg | 2 +- tox.ini | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.cfg b/setup.cfg index 03e4a918..4994681a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -45,7 +45,7 @@ install_requires = execnet>=1.1 pytest>=6.2.0 pytest-forked -setup_requires = setuptools_scm>=6.0 +setup_requires = # left empty, enforce using isolated build system [options.packages.find] where = src diff --git a/tox.ini b/tox.ini index 2578ae57..0c1655b5 100644 --- a/tox.ini +++ b/tox.ini @@ -5,7 +5,7 @@ envlist= py38-pytestmain py38-psutil py38-setproctitle - +isolated_build = true [testenv] extras = testing deps = From 1a5e680e9172fb536bd5e62fd3ed4f6b68284484 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Wed, 13 Jul 2022 20:54:26 +0200 Subject: [PATCH 111/113] Document the pytest_xdist_auto_num_workers hook (#791) --- changelog/791.doc | 1 + docs/distribution.rst | 18 +++++++++++++++--- 2 files changed, 16 insertions(+), 3 deletions(-) create mode 100644 changelog/791.doc diff --git a/changelog/791.doc b/changelog/791.doc new file mode 100644 index 00000000..adc3e65c --- /dev/null +++ b/changelog/791.doc @@ -0,0 +1 @@ +Document the ``pytest_xdist_auto_num_workers`` hook. diff --git a/docs/distribution.rst b/docs/distribution.rst index 5a867439..0ec9416d 100644 --- a/docs/distribution.rst +++ b/docs/distribution.rst @@ -5,12 +5,24 @@ Running tests across multiple CPUs To send tests to multiple CPUs, use the ``-n`` (or ``--numprocesses``) option:: - pytest -n 8 + pytest -n auto -Pass ``-n auto`` to use as many processes as your computer has CPU cores. This -can lead to considerable speed ups, especially if your test suite takes a +This can lead to considerable speed ups, especially if your test suite takes a noticeable amount of time. +With ``-n auto``, pytest-xdist will use as many processes as your computer +has CPU cores. +Pass a number, e.g. ``-n 8``, to specify the number of processes explicitly. + +To specify a different meaning for ``-n auto`` for your tests, +you can implement the ``pytest_xdist_auto_num_workers`` +`pytest hook `__ +(a function named ``pytest_xdist_auto_num_workers`` in e.g. ``conftest.py``) +that returns the number of processes to use. + + +Parallelization can be configured further with these options: + * ``--maxprocesses=maxprocesses``: limit the maximum number of workers to process the tests. From 61132777f8d85f7e03837684fbbdb41cb4f9c486 Mon Sep 17 00:00:00 2001 From: Pepa Date: Fri, 22 Jul 2022 15:44:17 +0200 Subject: [PATCH 112/113] Added known limitation section to the docs (#796) Co-authored-by: Josef Co-authored-by: Bruno Oliveira --- changelog/796.doc.rst | 1 + docs/index.rst | 1 + docs/known-limitations.rst | 52 ++++++++++++++++++++++++++++++++++++++ src/xdist/report.py | 3 ++- testing/test_dsession.py | 3 ++- 5 files changed, 58 insertions(+), 2 deletions(-) create mode 100644 changelog/796.doc.rst create mode 100644 docs/known-limitations.rst diff --git a/changelog/796.doc.rst b/changelog/796.doc.rst new file mode 100644 index 00000000..28a10bc4 --- /dev/null +++ b/changelog/796.doc.rst @@ -0,0 +1 @@ +Added known limitations section to documentation. diff --git a/docs/index.rst b/docs/index.rst index 94747b15..6e8e6112 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -60,4 +60,5 @@ Features crash how-to how-it-works + known-limitations changelog diff --git a/docs/known-limitations.rst b/docs/known-limitations.rst new file mode 100644 index 00000000..04641a7b --- /dev/null +++ b/docs/known-limitations.rst @@ -0,0 +1,52 @@ +Known limitations +================= + +pytest-xdist has some limitations that may be supported in pytest but can't be supported in pytest-xdist. + +Order and amount of test must be consistent +------------------------------------------- + +Is is not possible to have tests that differ in order or their amount across workers. + +This is especially true with ``pytest.mark.parametrize``, when values are produced with sets or other unordered iterables/generators. + + +Example: + +.. code-block:: python + + import pytest + + @pytest.mark.parametrize("param", {"a","b"}) + def test_pytest_parametrize_unordered(param): + pass + +In the example above, the fact that ``set`` are not necessarily ordered can cause different workers +to collect tests in different order, which will throw an error. + +Workarounds +~~~~~~~~~~~ + +A solution to this is to guarantee that the parametrized values have the same order. + +Some solutions: + +* Convert your sequence to a ``list``. + + .. code-block:: python + + import pytest + + @pytest.mark.parametrize("param", ["a", "b"]) + def test_pytest_parametrize_unordered(param): + pass + +* Sort your sequence, guaranteeing order. + + .. code-block:: python + + import pytest + + @pytest.mark.parametrize("param", sorted({"a", "b"})) + def test_pytest_parametrize_unordered(param): + pass diff --git a/src/xdist/report.py b/src/xdist/report.py index 02ad30d2..d956577d 100644 --- a/src/xdist/report.py +++ b/src/xdist/report.py @@ -14,7 +14,8 @@ def report_collection_diff(from_collection, to_collection, from_id, to_id): error_message = ( "Different tests were collected between {from_id} and {to_id}. " "The difference is:\n" - "{diff}" + "{diff}\n" + "To see why this happens see Known limitations in documentation" ).format(from_id=from_id, to_id=to_id, diff="\n".join(diff)) msg = "\n".join(x.rstrip() for x in error_message.split("\n")) return msg diff --git a/testing/test_dsession.py b/testing/test_dsession.py index 464045e7..d3a57152 100644 --- a/testing/test_dsession.py +++ b/testing/test_dsession.py @@ -290,7 +290,8 @@ def test_report_collection_diff_different() -> None: " bbb\n" "+XXX\n" " ccc\n" - "-YYY" + "-YYY\n" + "To see why this happens see Known limitations in documentation" ) msg = report_collection_diff(from_collection, to_collection, "1", "2") From 9236f11757a04d15af8731fbb2c92c5d4ebd5b14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sylvain=20Mari=C3=A9?= Date: Tue, 23 Aug 2022 22:41:23 +0200 Subject: [PATCH 113/113] Added a couple tests for #404 (#766) * Added a couple tests for #404 Co-authored-by: Sylvain MARIE --- testing/test_workermanage.py | 82 +++++++++++++++++++++++++++++++++++- testing/util.py | 9 ++++ 2 files changed, 90 insertions(+), 1 deletion(-) create mode 100644 testing/util.py diff --git a/testing/test_workermanage.py b/testing/test_workermanage.py index fae06011..b19c524a 100644 --- a/testing/test_workermanage.py +++ b/testing/test_workermanage.py @@ -3,9 +3,12 @@ import pytest import shutil import textwrap +import warnings from pathlib import Path +from util import generate_warning from xdist import workermanage -from xdist.workermanage import HostRSync, NodeManager +from xdist.remote import serialize_warning_message +from xdist.workermanage import HostRSync, NodeManager, unserialize_warning_message pytest_plugins = "pytester" @@ -326,3 +329,80 @@ def test_one(): ) (rep,) = reprec.getreports("pytest_runtest_logreport") assert rep.passed + + +class MyWarning(UserWarning): + pass + + +@pytest.mark.parametrize( + "w_cls", + [ + UserWarning, + MyWarning, + "Imported", + pytest.param( + "Nested", + marks=pytest.mark.xfail(reason="Nested warning classes are not supported."), + ), + ], +) +def test_unserialize_warning_msg(w_cls): + """Test that warning serialization process works well""" + + # Create a test warning message + with pytest.warns(UserWarning) as w: + if not isinstance(w_cls, str): + warnings.warn("hello", w_cls) + elif w_cls == "Imported": + generate_warning() + elif w_cls == "Nested": + # dynamic creation + class MyWarning2(UserWarning): + pass + + warnings.warn("hello", MyWarning2) + + # Unpack + assert len(w) == 1 + w_msg = w[0] + + # Serialize and deserialize + data = serialize_warning_message(w_msg) + w_msg2 = unserialize_warning_message(data) + + # Compare the two objects + all_keys = set(vars(w_msg).keys()).union(set(vars(w_msg2).keys())) + for k in all_keys: + v1 = getattr(w_msg, k) + v2 = getattr(w_msg2, k) + if k == "message": + assert type(v1) == type(v2) + assert v1.args == v2.args + else: + assert v1 == v2 + + +class MyWarningUnknown(UserWarning): + # Changing the __module__ attribute is only safe if class can be imported + # from there + __module__ = "unknown" + + +def test_warning_serialization_tweaked_module(): + """Test for GH#404""" + + # Create a test warning message + with pytest.warns(UserWarning) as w: + warnings.warn("hello", MyWarningUnknown) + + # Unpack + assert len(w) == 1 + w_msg = w[0] + + # Serialize and deserialize + data = serialize_warning_message(w_msg) + + # __module__ cannot be found! + with pytest.raises(ModuleNotFoundError): + unserialize_warning_message(data) diff --git a/testing/util.py b/testing/util.py new file mode 100644 index 00000000..c7bcc552 --- /dev/null +++ b/testing/util.py @@ -0,0 +1,9 @@ +import warnings + + +class MyWarning2(UserWarning): + pass + + +def generate_warning(): + warnings.warn(MyWarning2("hello"))