Compare commits

..

55 Commits

Author SHA1 Message Date
Bruno Oliveira
8f90fb73db Release 2.2.0
Some checks failed
build / build (ubuntu-latest, 3.5, py35-pytestlatest) (push) Failing after 1s
build / build (ubuntu-latest, 3.6, py36-pytestlatest) (push) Failing after 2s
build / build (ubuntu-latest, 3.7, linting) (push) Failing after 1s
build / build (ubuntu-latest, 3.7, py37-pytestlatest) (push) Failing after 1s
build / build (ubuntu-latest, 3.8, py38-psutil) (push) Failing after 1s
build / build (ubuntu-latest, 3.8, py38-pytestlatest) (push) Failing after 1s
build / build (ubuntu-latest, 3.8, py38-pytestmaster) (push) Failing after 1s
build / build (ubuntu-latest, 3.9, py39-pytestlatest) (push) Failing after 1s
build / linting (push) Failing after 1s
build / build (windows-latest, 3.5, py35-pytestlatest) (push) Has been cancelled
build / build (windows-latest, 3.6, py36-pytestlatest) (push) Has been cancelled
build / build (windows-latest, 3.7, linting) (push) Has been cancelled
build / build (windows-latest, 3.7, py37-pytestlatest) (push) Has been cancelled
build / build (windows-latest, 3.8, py38-psutil) (push) Has been cancelled
build / build (windows-latest, 3.8, py38-pytestlatest) (push) Has been cancelled
build / build (windows-latest, 3.8, py38-pytestmaster) (push) Has been cancelled
build / build (windows-latest, 3.9, py39-pytestlatest) (push) Has been cancelled
build / deploy (push) Has been cancelled
2020-12-14 09:07:49 -03:00
Bruno Oliveira
c5fadcd734 Merge pull request #608 from nicoddemus/better-errors
Propagate internal errors to the master node
2020-12-14 09:04:11 -03:00
Bruno Oliveira
16d636391b Propagate internal errors to the master node
This should help users diagnose internal errors in workers like
exceptions from hooks or in pytest itself.
2020-12-13 20:05:31 -03:00
Bruno Oliveira
948f13730d Merge pull request #609 from nicoddemus/gh-actions 2020-12-13 19:58:48 -03:00
Bruno Oliveira
db896a9d33 tox linting env expects Python 3.7 2020-12-12 12:06:33 -03:00
Bruno Oliveira
cf45eab977 Use GHA badge in README 2020-12-12 12:02:17 -03:00
Bruno Oliveira
00b0464d4b Remove travis and appveyor configs 2020-12-12 12:01:16 -03:00
Bruno Oliveira
b3a1bf3c2e 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
2020-12-12 11:58:41 -03:00
Bruno Oliveira
3fb369c8ac Fix linting 2020-12-12 11:52:47 -03:00
Bruno Oliveira
16694a978d Pass PRE_COMMIT_HOME to reuse local cache 2020-12-12 11:52:37 -03:00
Bruno Oliveira
2dcb40799f Setup GitHub Actions 2020-12-12 11:45:40 -03:00
Bruno Oliveira
14bafa6dd5 Rename HOWTORELEASE.rst to RELEASING.rst
To comply with pytest's nomenclature.
2020-09-22 09:45:02 -03:00
Bruno Oliveira
b51286aaa2 Merge pull request #599 from yili1992/master 2020-09-12 07:56:49 -03:00
易陆
03ddee6620 docs: fix invalid url 2020-09-10 10:16:16 +08:00
易陆
e03566b7d6 docs: fix invalid url 2020-09-10 10:14:37 +08:00
Bruno Oliveira
46cf204c64 Merge pull request #591 from nicoddemus/release-2.1.0
Release 2.1.0
2020-08-25 09:40:01 -03:00
Bruno Oliveira
5b7eeab7dc Release 2.1.0 2020-08-25 09:01:37 -03:00
Bruno Oliveira
9fd3b66292 Merge pull request #590 from nicoddemus/auto-hook 2020-08-25 09:00:38 -03:00
Bruno Oliveira
fe48256f1a Remove universal wheel setting: pytest-xdist is Python 3 only 2020-08-25 08:45:45 -03:00
Bruno Oliveira
607d8288d4 Add psutil extra and introduce pytest_xdist_auto_num_workers hook
This makes using psutil optional and opens up the possibility of customization
through the pytest_xdist_auto_num_workers hook, making things like #477 possible.

Fix #585
2020-08-25 08:38:28 -03:00
Bruno Oliveira
e469fc7b80 Revert "Merge pull request #560 from utapyngo/logical-cpu-count"
This reverts commit 0094b29a14, reversing
changes made to c6255faad4.
2020-08-24 14:50:36 -03:00
Bruno Oliveira
7caf7437e3 Merge pull request #588 from pelmini/fix_readme 2020-08-24 14:46:33 -03:00
pelmini
457386f837 Fixed typo in README 2020-08-23 17:15:02 -07:00
Bruno Oliveira
1d08799c82 Merge pull request #582 from nicoddemus/fix-docs
Fix session-scoped fixtures example in README
2020-08-15 07:56:28 -03:00
Bruno Oliveira
ac5b9956bb Fix session-scoped fixtures example in README 2020-08-15 07:43:11 -03:00
Bruno Oliveira
995b34aadc Merge pull request #575 from nicoddemus/release-2.0.0 2020-08-14 11:34:48 -03:00
Bruno Oliveira
43dad454c6 Prepare release 2.0.0 2020-08-12 17:28:27 -03:00
Christine M
c211d247a7 Noisy rsync (#473)
Co-authored-by: Bruno Oliveira <nicoddemus@gmail.com>
2020-08-12 17:23:03 -03:00
Thomas Grainger
2b8b8408bd run pyupgrade in ci (#573)
Co-authored-by: Bruno Oliveira <nicoddemus@gmail.com>
2020-08-06 13:03:06 -03:00
Bruno Oliveira
30606257fc Merge pull request #572 from pytest-dev/pytest6
Remove compat for pytest < 6
2020-08-06 09:12:58 -03:00
Zac-HD
d153e0a4c4 Remove compat for pytest < 6 2020-08-06 15:48:33 +10:00
Sylvain Marié
ba8398478e New methods is_xdist_worker, is_xdist_master, get_xdist_worker_id. (#505)
Co-authored-by: Sylvain MARIE <sylvain.marie@se.com>
Co-authored-by: Zac Hatfield-Dodds <Zac-HD@users.noreply.github.com>
Co-authored-by: Bruno Oliveira <nicoddemus@gmail.com>
2020-08-05 20:41:54 -03:00
Bruno Oliveira
abc1ae0e02 Merge pull request #571 from nicoddemus/remove-pytest-logwarning
Drop features from old pytest versions
2020-08-05 20:27:57 -03:00
Bruno Oliveira
047816f33a Drop features from old pytest versions 2020-08-05 19:55:56 -03:00
Bruno Oliveira
56a852665d Merge pull request #570 from nicoddemus/fix-config-init 2020-08-05 19:44:55 -03:00
Bruno Oliveira
55838a126d Remove unnecessary skipif and changelog 2020-08-05 19:24:53 -03:00
Michał Górny
bcfbf9f94b tox.ini: Extend testing to py39 2020-08-05 19:21:27 -03:00
Michał Górny
94bdd18a03 Fix skipping test_config_initialization for pytest<5
Fix test_config_initialization to be correctly skipped on pytest<5,
by explicitly checking pytest version (idea copied from
test_looponfail_removed_test).  The current conditions are insufficient
-- the outer check wrongly assumes pytest>=5 will always be used
on Python 3 (which is not true if you need the same version to support
both Python 2 and Python 3), and the inner condition apparently
wrongly assuming that invocation_params attribute is not present
in pytest-4.
2020-08-05 19:20:14 -03:00
Bruno Oliveira
fe459ba716 Merge pull request #541 from pytest-dev/emancipation 2020-08-05 19:18:02 -03:00
Bruno Oliveira
2f014c39df Minor changelog adjustment 2020-08-05 19:02:50 -03:00
Bruno Oliveira
e5cdb771f7 Add CHANGELOG about dropping Python 2.7
Fix #569
2020-08-05 19:01:57 -03:00
Bruno Oliveira
bdf66f2c8e Add CHANGELOG about requiring pytest 6.0+ 2020-08-05 18:57:25 -03:00
Hugo
62bc333e3e Fix tests 2020-08-03 22:52:35 +10:00
Hugo
dddac7c7e9 Remove six library 2020-08-03 22:52:18 +10:00
Hugo
9decca0023 Drop support for EOL Python 2 2020-08-03 22:52:03 +10:00
Hugo
22ae7ff7af Upgrade Python syntax with pyupgrade --py3-plus 2020-08-03 22:52:03 +10:00
Hugo
6f315356be Set py35 as target version for Black 2020-08-03 22:52:03 +10:00
Zac-HD
de3e54fd27 Finish removal of "slave" terminology
See issue #234.  It's time.
2020-08-03 22:51:42 +10:00
Zac-HD
f23c717a36 Delete old issues file 2020-08-03 22:50:13 +10:00
Zac Hatfield-Dodds
0094b29a14 Merge pull request #560 from utapyngo/logical-cpu-count 2020-08-03 22:49:09 +10:00
Ivan Zaikin
e528a42ddc Support OpenBSD and NetBSD 2020-07-28 08:55:32 +07:00
Bruno Oliveira
c6255faad4 Merge pull request #564 from nicoddemus/release-1.34.0
Release 1.34.0
2020-07-27 20:09:04 -03:00
Ivan Zaikin
3312dcde61 Apply black 2020-07-23 10:42:18 +07:00
Ivan Zaikin
1c85f1817a Add changelog 2020-07-23 10:42:04 +07:00
Ivan Zaikin
77e5fdfef9 Use psutil.cpu_count(logical=False) in auto_detect_cpus() 2020-07-23 10:18:43 +07:00
29 changed files with 464 additions and 351 deletions

View File

@@ -1,21 +0,0 @@
environment:
matrix:
- TOXENV: "py27-pytestlatest"
- TOXENV: "py35-pytestlatest"
- TOXENV: "py36-pytestlatest"
- TOXENV: "py37-pytestlatest"
- TOXENV: "py38-pytestlatest"
- TOXENV: "py38-pytestmaster"
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

99
.github/workflows/main.yml vendored Normal file
View File

@@ -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.7"
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 }}

View File

@@ -3,7 +3,7 @@ repos:
rev: 19.10b0 rev: 19.10b0
hooks: hooks:
- id: black - id: black
args: [--safe, --quiet] args: [--safe, --quiet, --target-version, py35]
language_version: python3.7 language_version: python3.7
- repo: https://github.com/pre-commit/pre-commit-hooks - repo: https://github.com/pre-commit/pre-commit-hooks
rev: v2.4.0 rev: v2.4.0
@@ -13,6 +13,11 @@ repos:
- id: check-yaml - id: check-yaml
- id: debug-statements - id: debug-statements
- id: flake8 - id: flake8
- repo: https://github.com/asottile/pyupgrade
rev: v2.7.2
hooks:
- id: pyupgrade
args: [--py3-plus]
- repo: local - repo: local
hooks: hooks:
- id: rst - id: rst

View File

@@ -1,62 +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
- python: '2.7'
env: TOXENV=py27-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.8"
env: TOXENV=py38-pytestmaster
- 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

View File

@@ -1,3 +1,60 @@
pytest-xdist 2.2.0 (2020-12-14)
===============================
Features
--------
- `#608 <https://github.com/pytest-dev/pytest-xdist/issues/608>`_: Internal errors in workers are now propagated to the master node.
pytest-xdist 2.1.0 (2020-08-25)
===============================
Features
--------
- `#585 <https://github.com/pytest-dev/pytest-xdist/issues/585>`_: New ``pytest_xdist_auto_num_workers`` hook can be implemented by plugins or ``conftest.py`` files to control the number of workers when ``--numprocesses=auto`` is given in the command-line.
Trivial Changes
---------------
- `#585 <https://github.com/pytest-dev/pytest-xdist/issues/585>`_: ``psutil`` has proven to make ``pytest-xdist`` installation in certain platforms and containers problematic, so to use it for automatic number of CPUs detection users need to install the ``psutil`` extra::
pip install pytest-xdist[psutil]
pytest-xdist 2.0.0 (2020-08-12)
===============================
Deprecations and Removals
-------------------------
- `#541 <https://github.com/pytest-dev/pytest-xdist/issues/541>`_: Drop backward-compatibility "slave" aliases related to worker nodes. We deliberately moved away from this terminology years ago, and it seems like the right time to finish the deprecation and removal process.
- `#569 <https://github.com/pytest-dev/pytest-xdist/issues/569>`_: ``pytest-xdist`` no longer supports Python 2.7.
Features
--------
- `#504 <https://github.com/pytest-dev/pytest-xdist/issues/504>`_: New functions ``xdist.is_xdist_worker``, ``xdist.is_xdist_master``, ``xdist.get_xdist_worker_id``, to easily identify the current node.
Bug Fixes
---------
- `#471 <https://github.com/pytest-dev/pytest-xdist/issues/471>`_: Fix issue with Rsync reporting in quiet mode.
- `#553 <https://github.com/pytest-dev/pytest-xdist/issues/553>`_: When using ``-n auto``, count the number of physical CPU cores instead of logical ones.
Trivial Changes
---------------
- `#541 <https://github.com/pytest-dev/pytest-xdist/issues/541>`_: ``pytest-xdist`` now requires ``pytest>=6.0``.
pytest-xdist 1.34.0 (2020-07-27) pytest-xdist 1.34.0 (2020-07-27)
================================ ================================

View File

@@ -1,31 +0,0 @@
next release critical
-----------------------------------------------
tag: bug
miserably fails: --dist=each --tx popen --tx socket=...
rename / hooks
-----------------------------------------------
tag: bug
node -> slave
transition for hooks?
configure_node -> configure_slave
allow to remotely run xdist tests with xdist
-----------------------------------------------
tag: feature
allow to run xdist own tests using its own mechanism.
currently this doesn't work because the remote side
has no pytest plugin. How to configure/do
register "xdist.plugin" on the remote side?
see to avoid any "from _pytest" internal imports
-----------------------------------------------
tag: feature
currently tests and even xdist core code imports
names from the internal _pytest namespace.
See to avoid it.

View File

@@ -11,13 +11,8 @@
:alt: Python versions :alt: Python versions
:target: https://pypi.python.org/pypi/pytest-xdist :target: https://pypi.python.org/pypi/pytest-xdist
.. image:: https://travis-ci.org/pytest-dev/pytest-xdist.svg?branch=master .. image:: https://github.com/pytest-dev/pytest-xdist/workflows/build/badge.svg
:alt: Travis CI build status :target: https://github.com/pytest-dev/pytest-xdist/actions
: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://img.shields.io/badge/code%20style-black-000000.svg .. image:: https://img.shields.io/badge/code%20style-black-000000.svg
:target: https://github.com/ambv/black :target: https://github.com/ambv/black
@@ -57,17 +52,18 @@ Install the plugin with::
pip install pytest-xdist pip install pytest-xdist
or use the package in develop/in-place mode with
a checkout of the `pytest-xdist repository`_ ::
pip install --editable . To use ``psutil`` for detection of the number of CPUs available, install the ``psutil`` extra::
pip install pytest-xdist[psutil]
.. _parallelization: .. _parallelization:
Speed up test runs by sending tests to multiple CPUs Speed up test runs by sending tests to multiple CPUs
---------------------------------------------------- ----------------------------------------------------
To send tests to multiple CPUs, use the ``-n`` (or ``-numprocesses``) option:: To send tests to multiple CPUs, use the ``-n`` (or ``--numprocesses``) option::
pytest -n NUMCPUS pytest -n NUMCPUS
@@ -126,7 +122,7 @@ Here is the code:
@pytest.fixture(scope="session") @pytest.fixture(scope="session")
def session_data(tmp_path_factory, worker_id): def session_data(tmp_path_factory, worker_id):
if not worker_id: if worker_id == "master":
# not executing in with multiple workers, just produce the data and let # not executing in with multiple workers, just produce the data and let
# pytest's fixture caching do its job # pytest's fixture caching do its job
return produce_expensive_data() return produce_expensive_data()
@@ -246,7 +242,7 @@ at once. The specifications strings use the `xspec syntax`_.
.. _`xspec syntax`: http://codespeak.net/execnet/basics.html#xspec .. _`xspec syntax`: http://codespeak.net/execnet/basics.html#xspec
.. _`socketserver.py`: http://bitbucket.org/hpk42/execnet/raw/2af991418160/execnet/script/socketserver.py .. _`socketserver.py`: https://raw.githubusercontent.com/pytest-dev/execnet/master/execnet/script/socketserver.py
.. _`execnet`: http://codespeak.net/execnet .. _`execnet`: http://codespeak.net/execnet
@@ -268,7 +264,7 @@ a test or fixture, you may use the ``worker_id`` fixture to do so:
When ``xdist`` is disabled (running with ``-n0`` for example), then When ``xdist`` is disabled (running with ``-n0`` for example), then
``worker_id`` will return ``"master"``. ``worker_id`` will return ``"master"``.
Additionally, worker processes have the following environment variables Worker processes also have the following environment variables
defined: defined:
* ``PYTEST_XDIST_WORKER``: the name of the worker, e.g., ``"gw2"``. * ``PYTEST_XDIST_WORKER``: the name of the worker, e.g., ``"gw2"``.
@@ -278,6 +274,34 @@ defined:
The information about the worker_id in a test is stored in the ``TestReport`` as The information about the worker_id in a test is stored in the ``TestReport`` as
well, under the ``worker_id`` attribute. 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_master(request_or_session) -> bool:
"""Return `True` if this is the xdist master, `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 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 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
"""
Uniquely identifying the current test run Uniquely identifying the current test run
----------------------------------------- -----------------------------------------

View File

@@ -1,6 +1,3 @@
[bdist_wheel]
universal = 1
[metadata] [metadata]
license_file = LICENSE license_file = LICENSE

View File

@@ -1,6 +1,6 @@
from setuptools import setup, find_packages from setuptools import setup, find_packages
install_requires = ["execnet>=1.1", "pytest>=4.4.0", "pytest-forked", "six"] install_requires = ["execnet>=1.1", "pytest>=6.0.0", "pytest-forked"]
with open("README.rst") as f: with open("README.rst") as f:
@@ -9,8 +9,7 @@ with open("README.rst") as f:
setup( setup(
name="pytest-xdist", name="pytest-xdist",
use_scm_version={"write_to": "src/xdist/_version.py"}, use_scm_version={"write_to": "src/xdist/_version.py"},
description="pytest xdist plugin for distributed testing" description="pytest xdist plugin for distributed testing and loop-on-failing modes",
" and loop-on-failing modes",
long_description=long_description, long_description=long_description,
license="MIT", license="MIT",
author="holger krekel and contributors", author="holger krekel and contributors",
@@ -19,12 +18,12 @@ setup(
platforms=["linux", "osx", "win32"], platforms=["linux", "osx", "win32"],
packages=find_packages(where="src"), packages=find_packages(where="src"),
package_dir={"": "src"}, package_dir={"": "src"},
extras_require={"testing": ["filelock"]}, extras_require={"testing": ["filelock"], "psutil": ["psutil>=3.0"]},
entry_points={ entry_points={
"pytest11": ["xdist = xdist.plugin", "xdist.looponfail = xdist.looponfail"] "pytest11": ["xdist = xdist.plugin", "xdist.looponfail = xdist.looponfail"]
}, },
zip_safe=False, zip_safe=False,
python_requires=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*", python_requires=">=3.5",
install_requires=install_requires, install_requires=install_requires,
setup_requires=["setuptools_scm"], setup_requires=["setuptools_scm"],
classifiers=[ classifiers=[
@@ -39,9 +38,8 @@ setup(
"Topic :: Software Development :: Quality Assurance", "Topic :: Software Development :: Quality Assurance",
"Topic :: Utilities", "Topic :: Utilities",
"Programming Language :: Python", "Programming Language :: Python",
"Programming Language :: Python :: 2",
"Programming Language :: Python :: 2.7",
"Programming Language :: Python :: 3", "Programming Language :: Python :: 3",
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.5",
"Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.7",

View File

@@ -1,3 +1,4 @@
from xdist.plugin import is_xdist_worker, is_xdist_master, get_xdist_worker_id
from xdist._version import version as __version__ from xdist._version import version as __version__
__all__ = ["__version__"] __all__ = ["__version__", "is_xdist_worker", "is_xdist_master", "get_xdist_worker_id"]

View File

@@ -10,14 +10,14 @@ from xdist.scheduler import (
) )
from six.moves.queue import Empty, Queue from queue import Empty, Queue
class Interrupted(KeyboardInterrupt): class Interrupted(KeyboardInterrupt):
""" signals an immediate interruption. """ """ signals an immediate interruption. """
class DSession(object): class DSession:
"""A pytest plugin which runs a distributed test session """A pytest plugin which runs a distributed test session
At the beginning of the test session this creates a NodeManager At the beginning of the test session this creates a NodeManager
@@ -150,9 +150,6 @@ class DSession(object):
node.workerinfo["id"] = node.gateway.id node.workerinfo["id"] = node.gateway.id
node.workerinfo["spec"] = node.gateway.spec node.workerinfo["spec"] = node.gateway.spec
# TODO: (#234 task) needs this for pytest. Remove when refactor in pytest repo
node.slaveinfo = node.workerinfo
self.config.hook.pytest_testnodeready(node=node) self.config.hook.pytest_testnodeready(node=node)
if self.shuttingdown: if self.shuttingdown:
node.shutdown() node.shutdown()
@@ -177,6 +174,24 @@ class DSession(object):
assert not crashitem, (crashitem, node) assert not crashitem, (crashitem, node)
self._active_nodes.remove(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): def worker_errordown(self, node, error):
"""Emitted by the WorkerController when a node dies.""" """Emitted by the WorkerController when a node dies."""
self.config.hook.pytest_testnodedown(node=node, error=error) self.config.hook.pytest_testnodedown(node=node, error=error)
@@ -271,11 +286,6 @@ class DSession(object):
assert not rep.passed assert not rep.passed
self._failed_worker_collectreport(node, rep) self._failed_worker_collectreport(node, rep)
def worker_logwarning(self, message, code, nodeid, fslocation):
"""Emitted when a node calls the pytest_logwarning hook."""
kwargs = dict(message=message, code=code, nodeid=nodeid, fslocation=fslocation)
self.config.hook.pytest_logwarning.call_historic(kwargs=kwargs)
def worker_warning_captured(self, warning_message, when, item): def worker_warning_captured(self, warning_message, when, item):
"""Emitted when a node calls the pytest_warning_captured hook (deprecated in 6.0).""" """Emitted when a node calls the pytest_warning_captured hook (deprecated in 6.0)."""
kwargs = dict(warning_message=warning_message, when=when, item=item) kwargs = dict(warning_message=warning_message, when=when, item=item)
@@ -336,7 +346,7 @@ class DSession(object):
self.config.hook.pytest_runtest_logreport(report=rep) self.config.hook.pytest_runtest_logreport(report=rep)
class TerminalDistReporter(object): class TerminalDistReporter:
def __init__(self, config): def __init__(self, config):
self.config = config self.config = config
self.tr = config.pluginmanager.getplugin("terminalreporter") self.tr = config.pluginmanager.getplugin("terminalreporter")

View File

@@ -6,7 +6,6 @@
processes) otherwise changes to source code can crash processes) otherwise changes to source code can crash
the controlling process which should best never happen. the controlling process which should best never happen.
""" """
from __future__ import print_function
import py import py
import pytest import pytest
import sys import sys
@@ -55,7 +54,7 @@ def looponfail_main(config):
print() print()
class RemoteControl(object): class RemoteControl:
def __init__(self, config): def __init__(self, config):
self.config = config self.config = config
self.failures = [] self.failures = []
@@ -165,7 +164,7 @@ def init_worker_session(channel, args, option_dict):
WorkerFailSession(config, channel).main() WorkerFailSession(config, channel).main()
class WorkerFailSession(object): class WorkerFailSession:
def __init__(self, config, channel): def __init__(self, config, channel):
self.config = config self.config = config
self.channel = channel self.channel = channel
@@ -220,7 +219,7 @@ class WorkerFailSession(object):
self.channel.send((trails, failreports, self.collection_failed)) self.channel.send((trails, failreports, self.collection_failed))
class StatRecorder(object): class StatRecorder:
def __init__(self, rootdirlist): def __init__(self, rootdirlist):
self.rootdirlist = rootdirlist self.rootdirlist = rootdirlist
self.statcache = {} self.statcache = {}

View File

@@ -55,3 +55,13 @@ def pytest_xdist_node_collection_finished(node, ids):
@pytest.mark.firstresult @pytest.mark.firstresult
def pytest_xdist_make_scheduler(config, log): def pytest_xdist_make_scheduler(config, log):
""" return a node scheduler implementation """ """ return a node scheduler implementation """
@pytest.mark.firstresult
def pytest_xdist_auto_num_workers(config):
"""
Return the number of workers to spawn when ``--numprocesses=auto`` is given in the
command-line.
.. versionadded:: 2.1
"""

View File

@@ -5,9 +5,21 @@ import py
import pytest import pytest
def auto_detect_cpus(): def pytest_xdist_auto_num_workers():
try:
import psutil
except ImportError:
pass
else:
count = psutil.cpu_count(logical=False) or psutil.cpu_count()
if count:
return count
try: try:
from os import sched_getaffinity from os import sched_getaffinity
def cpu_count():
return len(sched_getaffinity(0))
except ImportError: except ImportError:
if os.environ.get("TRAVIS") == "true": if os.environ.get("TRAVIS") == "true":
# workaround https://bitbucket.org/pypy/pypy/issues/2375 # workaround https://bitbucket.org/pypy/pypy/issues/2375
@@ -16,11 +28,6 @@ def auto_detect_cpus():
from os import cpu_count from os import cpu_count
except ImportError: except ImportError:
from multiprocessing import cpu_count from multiprocessing import cpu_count
else:
def cpu_count():
return len(sched_getaffinity(0))
try: try:
n = cpu_count() n = cpu_count()
except NotImplementedError: except NotImplementedError:
@@ -28,13 +35,9 @@ def auto_detect_cpus():
return n if n else 1 return n if n else 1
class AutoInt(int):
"""Mark value as auto-detected."""
def parse_numprocesses(s): def parse_numprocesses(s):
if s == "auto": if s == "auto":
return AutoInt(auto_detect_cpus()) return "auto"
elif s is not None: elif s is not None:
return int(s) return int(s)
@@ -62,14 +65,11 @@ def pytest_addoption(parser):
) )
group.addoption( group.addoption(
"--max-worker-restart", "--max-worker-restart",
"--max-slave-restart",
action="store", action="store",
default=None, default=None,
dest="maxworkerrestart", dest="maxworkerrestart",
help="maximum number of workers that can be restarted " help="maximum number of workers that can be restarted "
"when crashed (set to zero to disable this feature)\n" "when crashed (set to zero to disable this feature)",
"'--max-slave-restart' option is deprecated and will be removed in "
"a future release",
) )
group.addoption( group.addoption(
"--dist", "--dist",
@@ -190,12 +190,13 @@ def pytest_configure(config):
@pytest.mark.tryfirst @pytest.mark.tryfirst
def pytest_cmdline_main(config): def pytest_cmdline_main(config):
usepdb = config.getoption("usepdb", False) # a core option usepdb = config.getoption("usepdb", False) # a core option
if isinstance(config.option.numprocesses, AutoInt): if config.option.numprocesses == "auto":
if usepdb: if usepdb:
config.option.numprocesses = 0 config.option.numprocesses = 0
config.option.dist = "no" config.option.dist = "no"
else: else:
config.option.numprocesses = int(config.option.numprocesses) auto_num_cpus = config.hook.pytest_xdist_auto_num_workers(config=config)
config.option.numprocesses = auto_num_cpus
if config.option.numprocesses: if config.option.numprocesses:
if config.option.dist == "no": if config.option.dist == "no":
@@ -214,19 +215,53 @@ def pytest_cmdline_main(config):
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
# fixtures # fixtures and API to easily know the role of current node
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
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
"""
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
Note: this method also returns `False` when distribution has not been
activated at all.
:param request_or_session: the `pytest` `request` or `session` object
"""
return (
not is_xdist_worker(request_or_session)
and request_or_session.config.option.dist != "no"
)
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 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
"""
if hasattr(request_or_session.config, "workerinput"):
return request_or_session.config.workerinput["workerid"]
else:
return "master"
@pytest.fixture(scope="session") @pytest.fixture(scope="session")
def worker_id(request): def worker_id(request):
"""Return the id of the current worker ('gw0', 'gw1', etc) or 'master' """Return the id of the current worker ('gw0', 'gw1', etc) or 'master'
if running on the master node. if running on the master node.
""" """
if hasattr(request.config, "workerinput"): return get_xdist_worker_id(request)
return request.config.workerinput["workerid"]
else:
return "master"
@pytest.fixture(scope="session") @pytest.fixture(scope="session")

View File

@@ -11,14 +11,13 @@ import os
import time import time
import py import py
import _pytest.hookspec
import pytest import pytest
from execnet.gateway_base import dumps, DumpError from execnet.gateway_base import dumps, DumpError
from _pytest.config import _prepareconfig, Config from _pytest.config import _prepareconfig, Config
class WorkerInteractor(object): class WorkerInteractor:
def __init__(self, config, channel): def __init__(self, config, channel):
self.config = config self.config = config
self.workerid = config.workerinput.get("workerid", "?") self.workerid = config.workerinput.get("workerid", "?")
@@ -34,8 +33,10 @@ class WorkerInteractor(object):
self.channel.send((name, kwargs)) self.channel.send((name, kwargs))
def pytest_internalerror(self, excrepr): 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) self.log("IERROR>", line)
interactor.sendevent("internal_error", formatted_error=formatted_error)
def pytest_sessionstart(self, session): def pytest_sessionstart(self, session):
self.session = session self.session = session
@@ -101,11 +102,8 @@ class WorkerInteractor(object):
def pytest_runtest_logstart(self, nodeid, location): def pytest_runtest_logstart(self, nodeid, location):
self.sendevent("logstart", nodeid=nodeid, location=location) self.sendevent("logstart", nodeid=nodeid, location=location)
# the pytest_runtest_logfinish hook was introduced in pytest 3.4 def pytest_runtest_logfinish(self, nodeid, location):
if hasattr(_pytest.hookspec, "pytest_runtest_logfinish"): self.sendevent("logfinish", nodeid=nodeid, location=location)
def pytest_runtest_logfinish(self, nodeid, location):
self.sendevent("logfinish", nodeid=nodeid, location=location)
def pytest_runtest_logreport(self, report): def pytest_runtest_logreport(self, report):
data = self.config.hook.pytest_report_to_serializable( data = self.config.hook.pytest_report_to_serializable(
@@ -125,43 +123,14 @@ class WorkerInteractor(object):
) )
self.sendevent("collectreport", data=data) self.sendevent("collectreport", data=data)
# the pytest_logwarning hook was deprecated since pytest 4.0 def pytest_warning_recorded(self, warning_message, when, nodeid, location):
if hasattr( self.sendevent(
_pytest.hookspec, "pytest_logwarning" "warning_recorded",
) and not _pytest.hookspec.pytest_logwarning.pytest_spec.get("warn_on_impl"): warning_message_data=serialize_warning_message(warning_message),
when=when,
def pytest_logwarning(self, message, code, nodeid, fslocation): nodeid=nodeid,
self.sendevent( location=location,
"logwarning", )
message=message,
code=code,
nodeid=nodeid,
fslocation=str(fslocation),
)
# the pytest_warning_recorded hook was introduced in pytest 6.0
if hasattr(_pytest.hookspec, "pytest_warning_recorded"):
def pytest_warning_recorded(self, warning_message, when, nodeid, location):
self.sendevent(
"warning_recorded",
warning_message_data=serialize_warning_message(warning_message),
when=when,
nodeid=nodeid,
location=location,
)
# the pytest_warning_captured hook was introduced in pytest 3.8
elif hasattr(_pytest.hookspec, "pytest_warning_captured"):
def pytest_warning_captured(self, warning_message, when, item):
self.sendevent(
"warning_captured",
warning_message_data=serialize_warning_message(warning_message),
when=when,
# item cannot be serialized and will always be None when used with xdist
item=None,
)
def serialize_warning_message(warning_message): def serialize_warning_message(warning_message):
@@ -266,8 +235,5 @@ if __name__ == "__channelexec__":
config._parser.prog = os.path.basename(workerinput["mainargv"][0]) config._parser.prog = os.path.basename(workerinput["mainargv"][0])
config.workerinput = workerinput config.workerinput = workerinput
config.workeroutput = {} config.workeroutput = {}
# TODO: deprecated name, backward compatibility only. Remove it in future
config.slaveinput = config.workerinput
config.slaveoutput = config.workeroutput
interactor = WorkerInteractor(config, channel) interactor = WorkerInteractor(config, channel)
config.hook.pytest_cmdline_main(config=config) config.hook.pytest_cmdline_main(config=config)

View File

@@ -1,4 +1,3 @@
from __future__ import print_function
from difflib import unified_diff from difflib import unified_diff
@@ -13,9 +12,9 @@ def report_collection_diff(from_collection, to_collection, from_id, to_id):
diff = unified_diff(from_collection, to_collection, fromfile=from_id, tofile=to_id) diff = unified_diff(from_collection, to_collection, fromfile=from_id, tofile=to_id)
error_message = ( error_message = (
u"Different tests were collected between {from_id} and {to_id}. " "Different tests were collected between {from_id} and {to_id}. "
u"The difference is:\n" "The difference is:\n"
u"{diff}" "{diff}"
).format(from_id=from_id, to_id=to_id, diff="\n".join(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 return msg

View File

@@ -4,7 +4,7 @@ from xdist.workermanage import parse_spec_config
from xdist.report import report_collection_diff from xdist.report import report_collection_diff
class EachScheduling(object): class EachScheduling:
"""Implement scheduling of test items on all nodes """Implement scheduling of test items on all nodes
If a node gets added after the test run is started then it is If a node gets added after the test run is started then it is

View File

@@ -7,7 +7,7 @@ from xdist.workermanage import parse_spec_config
from xdist.report import report_collection_diff from xdist.report import report_collection_diff
class LoadScheduling(object): class LoadScheduling:
"""Implement load scheduling across nodes. """Implement load scheduling across nodes.
This distributes the tests collected across all nodes so each test This distributes the tests collected across all nodes so each test

View File

@@ -21,7 +21,7 @@ class LoadFileScheduling(LoadScopeScheduling):
""" """
def __init__(self, config, log=None): def __init__(self, config, log=None):
super(LoadFileScheduling, self).__init__(config, log) super().__init__(config, log)
if log is None: if log is None:
self.log = Producer("loadfilesched") self.log = Producer("loadfilesched")
else: else:

View File

@@ -6,7 +6,7 @@ from xdist.report import report_collection_diff
from xdist.workermanage import parse_spec_config from xdist.workermanage import parse_spec_config
class LoadScopeScheduling(object): class LoadScopeScheduling:
"""Implement load scheduling across nodes, but grouping test by scope. """Implement load scheduling across nodes, but grouping test by scope.
This distributes the tests collected across all nodes so each test is run This distributes the tests collected across all nodes so each test is run

View File

@@ -1,4 +1,3 @@
from __future__ import print_function
import fnmatch import fnmatch
import os import os
import re import re
@@ -29,7 +28,7 @@ def parse_spec_config(config):
return xspeclist return xspeclist
class NodeManager(object): class NodeManager:
EXIT_TIMEOUT = 10 EXIT_TIMEOUT = 10
DEFAULT_IGNORES = [".*", "*.pyc", "*.pyo", "*~"] DEFAULT_IGNORES = [".*", "*.pyc", "*.pyo", "*~"]
@@ -123,7 +122,7 @@ class NodeManager(object):
return { return {
"ignores": ignores, "ignores": ignores,
"verbose": getattr(self.config.option, "verbose", False), "verbose": getattr(self.config.option, "verbose", 0),
} }
def rsync(self, gateway, source, notify=None, verbose=False, ignores=None): def rsync(self, gateway, source, notify=None, verbose=False, ignores=None):
@@ -167,7 +166,7 @@ class HostRSync(execnet.RSync):
self._ignores = [ self._ignores = [
re.compile(fnmatch.translate(getattr(x, "strpath", x))) for x in ignores re.compile(fnmatch.translate(getattr(x, "strpath", x))) for x in ignores
] ]
super(HostRSync, self).__init__(sourcedir=sourcedir, **kwargs) super().__init__(sourcedir=sourcedir, **kwargs)
def filter(self, path): def filter(self, path):
path = py.path.local(path) path = py.path.local(path)
@@ -179,12 +178,10 @@ class HostRSync(execnet.RSync):
def add_target_host(self, gateway, finished=None): def add_target_host(self, gateway, finished=None):
remotepath = os.path.basename(self._sourcedir) remotepath = os.path.basename(self._sourcedir)
super(HostRSync, self).add_target( super().add_target(gateway, remotepath, finishedcallback=finished, delete=True)
gateway, remotepath, finishedcallback=finished, delete=True
)
def _report_send_file(self, gateway, modified_rel_path): def _report_send_file(self, gateway, modified_rel_path):
if self._verbose: if self._verbose > 0:
path = os.path.basename(self._sourcedir) + "/" + modified_rel_path path = os.path.basename(self._sourcedir) + "/" + modified_rel_path
remotepath = gateway.spec.chdir remotepath = gateway.spec.chdir
print("{}:{} <= {}".format(gateway.spec, remotepath, path)) print("{}:{} <= {}".format(gateway.spec, remotepath, path))
@@ -211,7 +208,7 @@ def make_reltoroot(roots, args):
return result return result
class WorkerController(object): class WorkerController:
ENDMARK = -1 ENDMARK = -1
class RemoteHook: class RemoteHook:
@@ -228,13 +225,9 @@ class WorkerController(object):
self.workerinput = { self.workerinput = {
"workerid": gateway.id, "workerid": gateway.id,
"workercount": len(nodemanager.specs), "workercount": len(nodemanager.specs),
"slaveid": gateway.id,
"slavecount": len(nodemanager.specs),
"testrunuid": nodemanager.testrunuid, "testrunuid": nodemanager.testrunuid,
"mainargv": sys.argv, "mainargv": sys.argv,
} }
# TODO: deprecated name, backward compatibility only. Remove it in future
self.slaveinput = self.workerinput
self._down = False self._down = False
self._shutdown_sent = False self._shutdown_sent = False
self.log = py.log.Producer("workerctl-%s" % gateway.id) self.log = py.log.Producer("workerctl-%s" % gateway.id)
@@ -296,7 +289,7 @@ class WorkerController(object):
if not self._down: if not self._down:
try: try:
self.sendcommand("shutdown") self.sendcommand("shutdown")
except (IOError, OSError): except OSError:
pass pass
self._shutdown_sent = True self._shutdown_sent = True
@@ -331,9 +324,11 @@ class WorkerController(object):
self.log("ignoring {}({})".format(eventname, kwargs)) self.log("ignoring {}({})".format(eventname, kwargs))
elif eventname == "workerready": elif eventname == "workerready":
self.notify_inproc(eventname, node=self, **kwargs) self.notify_inproc(eventname, node=self, **kwargs)
elif eventname == "internal_error":
self.notify_inproc(eventname, node=self, **kwargs)
elif eventname == "workerfinished": elif eventname == "workerfinished":
self._down = True self._down = True
self.workeroutput = self.slaveoutput = kwargs["workeroutput"] self.workeroutput = kwargs["workeroutput"]
self.notify_inproc("workerfinished", node=self) self.notify_inproc("workerfinished", node=self)
elif eventname in ("logstart", "logfinish"): elif eventname in ("logstart", "logfinish"):
self.notify_inproc(eventname, node=self, **kwargs) self.notify_inproc(eventname, node=self, **kwargs)
@@ -386,11 +381,7 @@ class WorkerController(object):
except: # noqa except: # noqa
from _pytest._code import ExceptionInfo from _pytest._code import ExceptionInfo
# ExceptionInfo API changed in pytest 4.1 excinfo = ExceptionInfo.from_current()
if hasattr(ExceptionInfo, "from_current"):
excinfo = ExceptionInfo.from_current()
else:
excinfo = ExceptionInfo()
print("!" * 20, excinfo) print("!" * 20, excinfo)
self.config.notify_exception(excinfo) self.config.notify_exception(excinfo)
self.shutdown() self.shutdown()

View File

@@ -1,10 +1,9 @@
import os import os
import re import re
import sys
import textwrap
import py import py
import pytest import pytest
import xdist
class TestDistribution: class TestDistribution:
@@ -115,9 +114,6 @@ class TestDistribution:
assert result.ret == 2 assert result.ret == 2
result.stdout.fnmatch_lines(["*Interrupted: stopping*1*", "*1 failed*"]) result.stdout.fnmatch_lines(["*Interrupted: stopping*1*", "*1 failed*"])
@pytest.mark.xfail(
reason="#527: Python 3.8 failure in pytest where testdir.tmpdir returns an unexpected value"
)
def test_basetemp_in_subprocesses(self, testdir): def test_basetemp_in_subprocesses(self, testdir):
p1 = testdir.makepyfile( p1 = testdir.makepyfile(
""" """
@@ -245,22 +241,6 @@ class TestDistribution:
result.stderr.fnmatch_lines(["--foobar=123 active! *"]) result.stderr.fnmatch_lines(["--foobar=123 active! *"])
assert dest.join(subdir.basename).check(dir=1) assert dest.join(subdir.basename).check(dir=1)
def test_backward_compatibility_worker_terminology(self, testdir):
"""Ensure that we still support "config.slaveinput" for backward compatibility (#234).
Keep in mind that removing this compatibility will break a ton of plugins and user code.
"""
testdir.makepyfile(
"""
def test(pytestconfig):
assert hasattr(pytestconfig, 'slaveinput')
assert hasattr(pytestconfig, 'workerinput')
"""
)
result = testdir.runpytest("-n1")
result.stdout.fnmatch_lines("*1 passed*")
assert result.ret == 0
def test_data_exchange(self, testdir): def test_data_exchange(self, testdir):
testdir.makeconftest( testdir.makeconftest(
""" """
@@ -593,16 +573,8 @@ def test_fixture_teardown_failure(testdir):
assert result.ret assert result.ret
@pytest.mark.skipif(
sys.version_info[:2] == (2, 7),
reason="Only available in pytest 5.0+ (Python 3 only)",
)
def test_config_initialization(testdir, monkeypatch, pytestconfig): def test_config_initialization(testdir, monkeypatch, pytestconfig):
"""Ensure workers and master are initialized consistently. Integration test for #445""" """Ensure workers and master are initialized consistently. Integration test for #445"""
if not hasattr(pytestconfig, "invocation_params"):
pytest.skip(
"requires pytest >=5.1 (config has no attribute 'invocation_params')"
)
testdir.makepyfile( testdir.makepyfile(
**{ **{
"dir_a/test_foo.py": """ "dir_a/test_foo.py": """
@@ -699,32 +671,6 @@ def test_skipping(testdir):
result.stdout.fnmatch_lines(["*hello*", "*1 skipped*"]) result.stdout.fnmatch_lines(["*hello*", "*1 skipped*"])
def test_issue34_pluginloading_in_subprocess(testdir):
import _pytest.hookspec
if not hasattr(_pytest.hookspec, "pytest_namespace"):
pytest.skip("this pytest version no longer supports pytest_namespace()")
testdir.tmpdir.join("plugin123.py").write(
textwrap.dedent(
"""
def pytest_namespace():
return {'sample_variable': 'testing'}
"""
)
)
testdir.makepyfile(
"""
import pytest
def test_hello():
assert pytest.sample_variable == "testing"
"""
)
result = testdir.runpytest_subprocess("-n1", "-p", "plugin123")
assert result.ret == 0
result.stdout.fnmatch_lines(["*1 passed*"])
def test_fixture_scope_caching_issue503(testdir): def test_fixture_scope_caching_issue503(testdir):
p1 = testdir.makepyfile( p1 = testdir.makepyfile(
""" """
@@ -802,27 +748,15 @@ def test_sub_plugins_disabled(testdir, plugin):
class TestWarnings: class TestWarnings:
@pytest.mark.parametrize("n", ["-n0", "-n1"]) @pytest.mark.parametrize("n", ["-n0", "-n1"])
@pytest.mark.parametrize("warn_type", ["pytest", "builtin"]) def test_warnings(self, testdir, n):
def test_warnings(self, testdir, n, request, warn_type):
if warn_type == "builtin":
warn_code = """warnings.warn(UserWarning('this is a warning'))"""
elif warn_type == "pytest":
if not hasattr(request.config, "warn"):
pytest.skip("config.warn has been removed in pytest 4.1")
warn_code = """request.config.warn('', 'this is a warning',
fslocation=py.path.local())"""
else:
assert False
testdir.makepyfile( testdir.makepyfile(
""" """
import warnings, py, pytest import warnings, py, pytest
@pytest.mark.filterwarnings('ignore:config.warn has been deprecated') @pytest.mark.filterwarnings('ignore:config.warn has been deprecated')
def test_func(request): def test_func(request):
{warn_code} warnings.warn(UserWarning('this is a warning'))
""".format( """
warn_code=warn_code
)
) )
result = testdir.runpytest(n) result = testdir.runpytest(n)
result.stdout.fnmatch_lines(["*this is a warning*", "*1 passed, 1 warning*"]) result.stdout.fnmatch_lines(["*this is a warning*", "*1 passed, 1 warning*"])
@@ -838,19 +772,23 @@ class TestWarnings:
testdir.makeconftest( testdir.makeconftest(
""" """
def pytest_warning_captured(): def pytest_warning_captured(warning_message):
assert False, "this hook should not be called in this version" 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( testdir.makepyfile(
""" """
import warnings import warnings
def test(): def test():
warnings.warn("custom warning") warnings.warn("my custom worker warning")
""" """
) )
result = testdir.runpytest("-n1") 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") result.stdout.no_fnmatch_line("*this hook should not be called in this version")
@pytest.mark.parametrize("n", ["-n0", "-n1"]) @pytest.mark.parametrize("n", ["-n0", "-n1"])
@@ -899,9 +837,6 @@ class TestWarnings:
"""Check that warnings with unserializable _WARNING_DETAILS are """Check that warnings with unserializable _WARNING_DETAILS are
handled correctly (#379). handled correctly (#379).
""" """
if sys.version_info[0] < 3:
# The issue is only present in Python 3 warnings
return
testdir.makepyfile( testdir.makepyfile(
""" """
import warnings, pytest import warnings, pytest
@@ -1203,6 +1138,18 @@ def test_internal_error_with_maxfail(testdir):
assert "INTERNALERROR" not in result.stderr.str() 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: class TestLoadScope:
def test_by_module(self, testdir): def test_by_module(self, testdir):
test_file = """ test_file = """
@@ -1417,7 +1364,7 @@ def parse_tests_and_workers_from_output(lines):
r""" r"""
\[(gw\d)\] # worker \[(gw\d)\] # worker
\s* \s*
(?:\[\s*\d+%\])? # progress indicator (pytest >=3.3) (?:\[\s*\d+%\])? # progress indicator
\s(.*?) # status string ("PASSED") \s(.*?) # status string ("PASSED")
\s(.*::.*) # nodeid \s(.*::.*) # nodeid
""", """,
@@ -1436,3 +1383,39 @@ def get_workers_and_test_count_by_prefix(prefix, lines, expected_status="PASSED"
if expected_status == status and nodeid.startswith(prefix): if expected_status == status and nodeid.startswith(prefix):
result[worker] = result.get(worker, 0) + 1 result[worker] = result.get(worker, 0) + 1
return result return result
class TestAPI:
@pytest.fixture
def fake_request(self):
class FakeOption:
def __init__(self):
self.dist = "load"
class FakeConfig:
def __init__(self):
self.workerinput = {"workerid": "gw5"}
self.option = FakeOption()
class FakeRequest:
def __init__(self):
self.config = FakeConfig()
return FakeRequest()
def test_is_xdist_worker(self, fake_request):
assert xdist.is_xdist_worker(fake_request)
del fake_request.config.workerinput
assert not xdist.is_xdist_worker(fake_request)
def test_is_xdist_master(self, fake_request):
assert not xdist.is_xdist_master(fake_request)
del fake_request.config.workerinput
assert xdist.is_xdist_master(fake_request)
fake_request.config.option.dist = "no"
assert not xdist.is_xdist_master(fake_request)
def test_get_xdist_worker_id(self, fake_request):
assert xdist.get_xdist_worker_id(fake_request) == "gw5"
del fake_request.config.workerinput
assert xdist.get_xdist_worker_id(fake_request) == "master"

View File

@@ -1,20 +1,9 @@
import six
import py import py
import pytest import pytest
import execnet import execnet
pytest_plugins = "pytester" pytest_plugins = "pytester"
if six.PY2:
@pytest.fixture(scope="session", autouse=True)
def _ensure_imports():
# we import some modules because pytest-2.8's testdir fixture
# will unload all modules after each test and this cause
# (unknown) problems with execnet.Group()
execnet.Group
execnet.makegateway
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def _divert_atexit(request, monkeypatch): def _divert_atexit(request, monkeypatch):

View File

@@ -203,7 +203,7 @@ class TestLoadScheduling:
different test ids are collected by workers. different test ids are collected by workers.
""" """
class CollectHook(object): class CollectHook:
""" """
Dummy hook that stores collection reports. Dummy hook that stores collection reports.
""" """

View File

@@ -1,7 +1,11 @@
from contextlib import suppress
import py import py
import execnet import execnet
from xdist.workermanage import NodeManager from xdist.workermanage import NodeManager
import pytest
def test_dist_incompatibility_messages(testdir): def test_dist_incompatibility_messages(testdir):
result = testdir.runpytest("--pdb", "--looponfail") result = testdir.runpytest("--pdb", "--looponfail")
@@ -38,6 +42,11 @@ def test_auto_detect_cpus(testdir, monkeypatch):
import os import os
from xdist.plugin import pytest_cmdline_main as check_options from xdist.plugin import pytest_cmdline_main as check_options
with suppress(ImportError):
import psutil
monkeypatch.setattr(psutil, "cpu_count", lambda logical=True: None)
if hasattr(os, "sched_getaffinity"): if hasattr(os, "sched_getaffinity"):
monkeypatch.setattr(os, "sched_getaffinity", lambda _pid: set(range(99))) monkeypatch.setattr(os, "sched_getaffinity", lambda _pid: set(range(99)))
elif hasattr(os, "cpu_count"): elif hasattr(os, "cpu_count"):
@@ -51,6 +60,7 @@ def test_auto_detect_cpus(testdir, monkeypatch):
assert config.getoption("numprocesses") == 2 assert config.getoption("numprocesses") == 2
config = testdir.parseconfigure("-nauto") config = testdir.parseconfigure("-nauto")
check_options(config)
assert config.getoption("numprocesses") == 99 assert config.getoption("numprocesses") == 99
config = testdir.parseconfigure("-nauto", "--pdb") config = testdir.parseconfigure("-nauto", "--pdb")
@@ -62,9 +72,36 @@ def test_auto_detect_cpus(testdir, monkeypatch):
monkeypatch.delattr(os, "sched_getaffinity", raising=False) monkeypatch.delattr(os, "sched_getaffinity", raising=False)
monkeypatch.setenv("TRAVIS", "true") monkeypatch.setenv("TRAVIS", "true")
config = testdir.parseconfigure("-nauto") config = testdir.parseconfigure("-nauto")
check_options(config)
assert config.getoption("numprocesses") == 2 assert config.getoption("numprocesses") == 2
def test_auto_detect_cpus_psutil(testdir, monkeypatch):
from xdist.plugin import pytest_cmdline_main as check_options
psutil = pytest.importorskip("psutil")
monkeypatch.setattr(psutil, "cpu_count", lambda logical=True: 42)
config = testdir.parseconfigure("-nauto")
check_options(config)
assert config.getoption("numprocesses") == 42
def test_hook_auto_num_workers(testdir, monkeypatch):
from xdist.plugin import pytest_cmdline_main as check_options
testdir.makeconftest(
"""
def pytest_xdist_auto_num_workers():
return 42
"""
)
config = testdir.parseconfigure("-nauto")
check_options(config)
assert config.getoption("numprocesses") == 42
def test_boxed_with_collect_only(testdir): def test_boxed_with_collect_only(testdir):
from xdist.plugin import pytest_cmdline_main as check_options from xdist.plugin import pytest_cmdline_main as check_options

View File

@@ -8,7 +8,7 @@ from xdist.workermanage import WorkerController
import execnet import execnet
import marshal import marshal
from six.moves.queue import Queue from queue import Queue
WAIT_TIMEOUT = 10.0 WAIT_TIMEOUT = 10.0

View File

@@ -1,4 +1,3 @@
from __future__ import print_function
import py import py
import pytest import pytest
import textwrap import textwrap
@@ -35,7 +34,7 @@ def mysetup(tmpdir):
@pytest.fixture @pytest.fixture
def workercontroller(monkeypatch): def workercontroller(monkeypatch):
class MockController(object): class MockController:
def __init__(self, *args): def __init__(self, *args):
pass pass
@@ -197,6 +196,25 @@ class TestNodeManager:
assert dest.join("dir1", "dir2", "hello").check() assert dest.join("dir1", "dir2", "hello").check()
nodemanager.teardown_nodes() 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
if flag:
args += (flag,)
nodemanager = NodeManager(testdir.parseconfig(*args))
nodemanager.setup_nodes(None) # calls .rsync_roots()
out, _ = capsys.readouterr()
if expects_report:
assert "<= pytest/__init__.py" in out
else:
assert "<= pytest/__init__.py" not in out
def test_init_rsync_roots(self, testdir, mysetup, workercontroller): def test_init_rsync_roots(self, testdir, mysetup, workercontroller):
source, dest = mysetup.source, mysetup.dest source, dest = mysetup.source, mysetup.dest
dir2 = source.ensure("dir1", "dir2", dir=1) dir2 = source.ensure("dir1", "dir2", dir=1)

19
tox.ini
View File

@@ -1,11 +1,11 @@
[tox] [tox]
envlist= envlist=
linting linting
py{27,35,36,37,38}-pytestlatest py{35,36,37,38,39}-pytestlatest
py38-pytestmaster py38-pytestmaster
py38-psutil
[testenv] [testenv]
passenv = USER USERNAME
extras = testing extras = testing
deps = deps =
pytestlatest: pytest pytestlatest: pytest
@@ -13,9 +13,18 @@ deps =
commands= commands=
pytest {posargs} pytest {posargs}
[testenv:py38-psutil]
extras =
testing
psutil
deps = pytest
commands =
pytest {posargs:-k psutil}
[testenv:linting] [testenv:linting]
skipsdist = True skip_install = True
usedevelop = True usedevelop = True
passenv = PRE_COMMIT_HOME
deps = deps =
pre-commit pre-commit
commands = pre-commit run --all-files --show-diff-on-failure commands = pre-commit run --all-files --show-diff-on-failure
@@ -28,9 +37,9 @@ skipsdist = True
usedevelop = True usedevelop = True
passenv = * passenv = *
deps = deps =
towncrier towncrier
commands = commands =
towncrier --version {posargs} --yes towncrier --version {posargs} --yes
[pytest] [pytest]
addopts = -ra addopts = -ra