Compare commits

...

81 Commits

Author SHA1 Message Date
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
Bruno Oliveira
7bf654775d Release 1.34.0 2020-07-27 19:36:42 -03:00
Adam Johnson
b524317863 Make "--pdb" imply "--dist no"
Co-authored-by: Bruno Oliveira <nicoddemus@gmail.com>
2020-07-24 20:07:42 -03:00
Daniel Hahler
467ac33d0b tests: check that PYTEST_ADDOPTS is not processed twice 2020-07-24 19:04:16 -03:00
Bruno Oliveira
fac0c14e28 Merge pull request #531 from mgorny/timeout 2020-07-24 15:47:20 -03:00
Adam Johnson
82730e5c6c Improve description of testing with multiple CPU's
Co-authored-by: Bruno Oliveira <nicoddemus@gmail.com>
2020-07-24 15:46:49 -03:00
Bruno Oliveira
78eaa4c0e2 Merge pull request #563 from nicoddemus/pytest-warning-captured-562
Do not trigger pytest_warning_captured in pytest 6.0+
2020-07-24 15:40:19 -03:00
Bruno Oliveira
18b18870a8 Do not trigger pytest_warning_captured in pytest 6.0+
Fix #562
2020-07-24 15:19:07 -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
Bruno Oliveira
2fc21fddbb Merge pull request #558 from nicoddemus/fix-rsync-dirs
Fix rsyncdirs usage with pytest 6.0
2020-07-13 15:09:48 -03:00
Bruno Oliveira
443dc21061 Fix rsyncdirs usage with pytest 6.0
Fix pytest-dev/pytest#7487
2020-07-13 12:09:02 -03:00
Bruno Oliveira
727c41e957 Merge pull request #556 from nicoddemus/release-1.33.0 2020-07-09 20:47:50 -03:00
Bruno Oliveira
7396ffb7e8 Release 1.33.0 2020-07-09 20:20:02 -03:00
Bruno Oliveira
fbfe3b9a91 Add missing changelog for #548 2020-07-09 20:19:17 -03:00
Bruno Oliveira
075a74202b Add missing changelog for #554 2020-07-09 20:14:37 -03:00
Bruno Oliveira
b8e1e63c7a Merge pull request #554 from nicoddemus/fix-compat-pytest-master 2020-07-09 16:49:50 -03:00
Bruno Oliveira
bc0866f216 Merge pull request #548 from swt2c/exclude_scm_ci_files
Exclude SCM and CI files from distribution
2020-07-08 18:12:33 -03:00
Bruno Oliveira
b25757199c Support new `pytest_warning_recorded` hook from pytest 6.0 2020-07-08 17:51:45 -03:00
Bruno Oliveira
77a1db27cd Fix compatibility with pytest master
Fix the autouse fixture which diverts atexit.register calls to support the correct signature.
2020-07-08 15:58:31 -03:00
Bruno Oliveira
f5021313ec Merge pull request #545 from lgeiger/list-comprehensions
Prefer list comprehensions over list.append()
2020-07-08 15:44:25 -03:00
Scott Talbert
925829cb72 Exclude SCM and CI files from distribution
These files don't make sense outside of the git respository.
2020-06-26 12:02:03 -04:00
Lukas Geiger
311cbfabfb Prefer list comprehensions over list.append() 2020-06-21 01:00:02 +02:00
Bruno Oliveira
e48491d4fd Fix linting 2020-05-03 09:38:57 -03:00
Michał Górny
27519e8018 Increase pexpect timeouts to prevent tests from failing 2020-05-03 11:24:03 +02:00
Bruno Oliveira
59caed8e2f Merge pull request #530 from pytest-dev/release-1.32.0 2020-05-02 20:50:47 -03:00
Sven-Hendrik Haase
d3e1a52caa Prepare version 1.32.0 2020-05-03 01:00:40 +02:00
Bruno Oliveira
905eb3244d Merge pull request #528 from ashokdelphia/fix-acessing-typo-in-readme 2020-05-02 11:41:18 -03:00
Ashok Argent-Katwala
b4fb97a6e0 Fix typo in readme. 2020-05-02 11:40:23 -03:00
Sven-Hendrik Haase
e8bbb4db2a Merge pull request #525 from pytest-dev/524-add-global-session-uuid
Add global testrun uid
2020-05-01 19:36:53 +02:00
Sven-Hendrik Haase
069f959690 Fix docs format 2020-05-01 18:56:09 +02:00
Sven-Hendrik Haase
f56596bcdc Merge branch 'master' into 524-add-global-session-uuid 2020-05-01 16:56:23 +02:00
Sven-Hendrik Haase
ec13ceaa0c Merge pull request #527 from pytest-dev/ignore-python38-failure
Ignore Python 3.8 specific failure in pytest for now
2020-05-01 16:54:21 +02:00
Sven-Hendrik Haase
8714a99d51 Ignore Python 3.8 specific failure in pytest for now 2020-04-30 21:08:21 +02:00
Sven-Hendrik Haase
2183d741bb Add global session uuid (fixes #524) 2020-04-30 19:43:16 +02:00
Daniel Hahler
453cbd88dd tox/ci: remove testing with pytest's features branch (#509)
pytest is using the master branch now only.
2020-03-31 11:11:05 +02:00
Bruno Oliveira
bea6f66174 Merge pull request #475 from hugovk/rm-3.4
Drop support for EOL Python 3.4
2020-03-28 23:01:12 -03:00
Hugo van Kemenade
3a470043c9 Merge branch 'master' into rm-3.4 2020-01-26 21:05:51 +02:00
Bruno Oliveira
6fd5b56491 Merge pull request #495 from nicoddemus/release-1.31.0
Release 1.31.0
2019-12-19 22:45:30 -03:00
Hugo
69d98fb079 Format with Black 2019-12-02 18:00:05 +02:00
Hugo
db5532cba3 Add news file 2019-12-02 17:57:52 +02:00
Hugo
157871c026 Upgrade Python syntax with pyupgrade 2019-12-02 17:57:38 +02:00
Hugo
cf59eb07b9 Remove: '[WARNING] Unexpected key(s) present on local => rst: python_version' 2019-12-02 17:56:16 +02:00
Hugo
8f1e6ad10c Drop support for EOL Python 3.4 2019-12-02 17:56:08 +02:00
Hugo
3a87f367fe pre-commit autoupdate 2019-12-02 17:53:52 +02:00
30 changed files with 615 additions and 324 deletions

View File

@@ -1,13 +1,11 @@
environment: environment:
matrix: matrix:
- TOXENV: "py27-pytestlatest"
- TOXENV: "py34-pytestlatest"
- TOXENV: "py35-pytestlatest" - TOXENV: "py35-pytestlatest"
- TOXENV: "py36-pytestlatest" - TOXENV: "py36-pytestlatest"
- TOXENV: "py37-pytestlatest" - TOXENV: "py37-pytestlatest"
- TOXENV: "py38-pytestlatest" - TOXENV: "py38-pytestlatest"
- TOXENV: "py38-pytestmaster" - TOXENV: "py38-pytestmaster"
- TOXENV: "py38-pytestfeatures" - TOXENV: "py38-psutil"
install: install:
- C:\Python38\python -m pip install -U pip setuptools virtualenv - C:\Python38\python -m pip install -U pip setuptools virtualenv

View File

@@ -1,18 +1,23 @@
repos: repos:
- repo: https://github.com/ambv/black - repo: https://github.com/ambv/black
rev: 18.6b4 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: v1.3.0 rev: v2.4.0
hooks: hooks:
- id: trailing-whitespace - id: trailing-whitespace
- id: end-of-file-fixer - id: end-of-file-fixer
- 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

@@ -32,22 +32,20 @@ jobs:
- $HOME/.cache/pre-commit - $HOME/.cache/pre-commit
- python: '3.8' - python: '3.8'
env: TOXENV=py38-pytestlatest env: TOXENV=py38-pytestlatest
- python: '2.7'
env: TOXENV=py27-pytestlatest
- stage: test - stage: test
python: "3.4" python: "3.5"
env: TOXENV=py34-pytestlatest
- python: "3.5"
env: TOXENV=py35-pytestlatest env: TOXENV=py35-pytestlatest
- python: "3.6" - python: "3.6"
env: TOXENV=py36-pytestlatest env: TOXENV=py36-pytestlatest
- python: "3.7" - python: "3.7"
env: TOXENV=py37-pytestlatest env: TOXENV=py37-pytestlatest
- python: "3.9-dev"
env: TOXENV=py39-pytestlatest
- python: "3.8" - python: "3.8"
env: TOXENV=py38-pytestmaster env: TOXENV=py38-pytestmaster
- python: "3.8" - python: "3.8"
env: TOXENV=py38-pytestfeatures env: TOXENV=py38-psutil
- stage: deploy - stage: deploy
python: '3.8' python: '3.8'

View File

@@ -1,3 +1,101 @@
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)
================================
Features
--------
- `#549 <https://github.com/pytest-dev/pytest-xdist/issues/549>`_: Make ``--pdb`` imply ``--dist no``, as the two options cannot really work together at the moment.
Bug Fixes
---------
- `#478 <https://github.com/pytest-dev/pytest-xdist/issues/478>`_: Fix regression with duplicated arguments via $PYTEST_ADDOPTS in 1.30.0.
- `#558 <https://github.com/pytest-dev/pytest-xdist/issues/558>`_: Fix ``rsyncdirs`` usage with pytest 6.0.
- `#562 <https://github.com/pytest-dev/pytest-xdist/issues/562>`_: Do not trigger the deprecated ``pytest_warning_captured`` in pytest 6.0+.
pytest-xdist 1.33.0 (2020-07-09)
================================
Features
--------
- `#554 <https://github.com/pytest-dev/pytest-xdist/issues/554>`_: Fix warnings support for upcoming pytest 6.0.
Trivial Changes
---------------
- `#548 <https://github.com/pytest-dev/pytest-xdist/issues/548>`_: SCM and CI files are no longer included in the source distribution.
pytest-xdist 1.32.0 (2020-05-03)
================================
Deprecations and Removals
-------------------------
- `#475 <https://github.com/pytest-dev/pytest-xdist/issues/475>`_: Drop support for EOL Python 3.4.
Features
--------
- `#524 <https://github.com/pytest-dev/pytest-xdist/issues/524>`_: Add `testrun_uid` fixture. This is a shared value that uniquely identifies a test run among all workers.
This also adds a `PYTEST_XDIST_TESTRUNUID` environment variable that is accessible within a test as well as a command line option `--testrunuid` to manually set the value from outside.
pytest-xdist 1.31.0 (2019-12-19) pytest-xdist 1.31.0 (2019-12-19)
================================ ================================

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.

5
MANIFEST.in Normal file
View File

@@ -0,0 +1,5 @@
exclude .appveyor.yml
exclude .gitignore
exclude .pre-commit-config.yaml
exclude .travis.yml
prune .github

View File

@@ -57,43 +57,46 @@ 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, type:: To send tests to multiple CPUs, use the ``-n`` (or ``--numprocesses``) option::
pytest -n NUM pytest -n NUMCPUS
Especially for longer running tests or tests requiring Pass ``-n auto`` to use as many processes as your computer has CPU cores. This
a lot of I/O this can lead to considerable speed ups. This option can can lead to considerable speed ups, especially if your test suite takes a
also be set to ``auto`` for automatic detection of the number of CPUs. noticeable amount of time.
If a test crashes the interpreter, pytest-xdist will automatically restart If a test crashes a worker, pytest-xdist will automatically restart that worker
that worker and report the failure as usual. You can use the and report the tests failure. You can use the ``--max-worker-restart`` option
``--max-worker-restart`` option to limit the number of workers that can to limit the number of worker restarts that are allowed, or disable restarting
be restarted, or disable restarting altogether using ``--max-worker-restart=0``. altogether using ``--max-worker-restart 0``.
By default, the ``-n`` option will send pending tests to any worker that is available, without By default, using ``--numprocesses`` will send pending tests to any worker that
any guaranteed order, but you can control this with these options: is available, without any guaranteed order. You can change the test
distribution algorithm this with the ``--dist`` option. It takes these values:
* ``--dist=loadscope``: tests will be grouped by **module** for *test functions* and * ``--dist no``: The default algorithm, distributing one test at a time.
by **class** for *test methods*, then each group will be sent to an available worker,
guaranteeing 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. Currently the groupings can't be customized,
with grouping by class takes priority over grouping by module.
This feature was added in version ``1.19``.
* ``--dist=loadfile``: tests will be grouped by file name, and then will be sent to an available * ``--dist loadscope``: Tests are grouped by **module** for *test functions*
worker, guaranteeing that all tests in a group run in the same worker. This feature was added and by **class** for *test methods*. Groups are distributed to available
in version ``1.21``. 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.
Making session-scoped fixtures execute only once Making session-scoped fixtures execute only once
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@@ -124,7 +127,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()
@@ -266,7 +269,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"``.
@@ -276,8 +279,71 @@ 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.
Acessing ``sys.argv`` from the master node in workers 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
-----------------------------------------
*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 master 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 master node, use
``request.config.workerinput["mainargv"]``. ``request.config.workerinput["mainargv"]``.

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.*", python_requires=">=3.5",
install_requires=install_requires, install_requires=install_requires,
setup_requires=["setuptools_scm"], setup_requires=["setuptools_scm"],
classifiers=[ classifiers=[
@@ -39,10 +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.4", "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()
@@ -169,7 +166,7 @@ class DSession(object):
""" """
self.config.hook.pytest_testnodedown(node=node, error=None) self.config.hook.pytest_testnodedown(node=node, error=None)
if node.workeroutput["exitstatus"] == 2: # keyboard-interrupt if node.workeroutput["exitstatus"] == 2: # keyboard-interrupt
self.shouldstop = "%s received keyboard-interrupt" % (node,) self.shouldstop = "{} received keyboard-interrupt".format(node)
self.worker_errordown(node, "keyboard-interrupt") self.worker_errordown(node, "keyboard-interrupt")
return return
if node in self.sched.nodes: if node in self.sched.nodes:
@@ -271,16 +268,18 @@ 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_logwarning hook.""" """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)
self.config.hook.pytest_warning_captured.call_historic(kwargs=kwargs) self.config.hook.pytest_warning_captured.call_historic(kwargs=kwargs)
def worker_warning_recorded(self, warning_message, when, nodeid, location):
"""Emitted when a node calls the pytest_warning_recorded hook."""
kwargs = dict(
warning_message=warning_message, when=when, nodeid=nodeid, location=location
)
self.config.hook.pytest_warning_recorded.call_historic(kwargs=kwargs)
def _clone_node(self, node): def _clone_node(self, node):
"""Return new node based on an existing one. """Return new node based on an existing one.
@@ -321,7 +320,7 @@ class DSession(object):
# XXX count no of failures and retry N times # XXX count no of failures and retry N times
runner = self.config.pluginmanager.getplugin("runner") runner = self.config.pluginmanager.getplugin("runner")
fspath = nodeid.split("::")[0] fspath = nodeid.split("::")[0]
msg = "worker %r crashed while running %r" % (worker.gateway.id, nodeid) msg = "worker {!r} crashed while running {!r}".format(worker.gateway.id, nodeid)
rep = runner.TestReport( rep = runner.TestReport(
nodeid, (fspath, None, fspath), (), "failed", msg, "???" nodeid, (fspath, None, fspath), (), "failed", msg, "???"
) )
@@ -329,7 +328,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")
@@ -351,7 +350,9 @@ class TerminalDistReporter(object):
def getstatus(self): def getstatus(self):
if self.config.option.verbose >= 0: if self.config.option.verbose >= 0:
parts = ["%s %s" % (spec.id, self._status[spec.id]) for spec in self._specs] parts = [
"{} {}".format(spec.id, self._status[spec.id]) for spec in self._specs
]
return " / ".join(parts) return " / ".join(parts)
else: else:
return "bringing up nodes..." return "bringing up nodes..."
@@ -386,14 +387,16 @@ class TerminalDistReporter(object):
def pytest_testnodeready(self, node): def pytest_testnodeready(self, node):
if self.config.option.verbose > 0: if self.config.option.verbose > 0:
d = node.workerinfo d = node.workerinfo
infoline = "[%s] Python %s" % (d["id"], d["version"].replace("\n", " -- ")) infoline = "[{}] Python {}".format(
d["id"], d["version"].replace("\n", " -- ")
)
self.rewrite(infoline, newline=True) self.rewrite(infoline, newline=True)
self.setstatus(node.gateway.spec, "ok") self.setstatus(node.gateway.spec, "ok")
def pytest_testnodedown(self, node, error): def pytest_testnodedown(self, node, error):
if not error: if not error:
return return
self.write_line("[%s] node down: %s" % (node.gateway.id, error)) self.write_line("[{}] node down: {}".format(node.gateway.id, error))
def get_default_max_worker_restart(config): def get_default_max_worker_restart(config):

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 = []
@@ -136,7 +135,7 @@ def repr_pytest_looponfailinfo(failreports, rootdirs):
tr.line(report, red=True) tr.line(report, red=True)
tr.sep("#", "waiting for changes", bold=True) tr.sep("#", "waiting for changes", bold=True)
for rootdir in rootdirs: for rootdir in rootdirs:
tr.line("### Watching: %s" % (rootdir,), bold=True) tr.line("### Watching: {}".format(rootdir), bold=True)
def init_worker_session(channel, args, option_dict): def init_worker_session(channel, args, option_dict):
@@ -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

@@ -1,12 +1,25 @@
import os import os
import uuid
import py 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
@@ -15,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:
@@ -27,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)
@@ -61,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",
@@ -122,12 +123,22 @@ def pytest_addoption(parser):
metavar="GLOB", metavar="GLOB",
help="add expression for ignores when rsyncing to remote tx nodes.", help="add expression for ignores when rsyncing to remote tx nodes.",
) )
group.addoption( group.addoption(
"--boxed", "--boxed",
action="store_true", action="store_true",
help="backward compatibility alias for pytest-forked --forked", help="backward compatibility alias for pytest-forked --forked",
) )
group.addoption(
"--testrunuid",
action="store",
help=(
"provide an identifier shared amongst all workers as the value of "
"the 'testrun_uid' fixture,\n\n,"
"if not provided, 'testrun_uid' is filled with a new unique string "
"on every test run."
),
)
parser.addini( parser.addini(
"rsyncdirs", "rsyncdirs",
"list of (relative) paths to be rsynced for remote distributed testing.", "list of (relative) paths to be rsynced for remote distributed testing.",
@@ -179,8 +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":
config.option.numprocesses = 0 if usepdb else int(config.option.numprocesses) if usepdb:
config.option.numprocesses = 0
config.option.dist = "no"
else:
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":
@@ -192,25 +208,66 @@ def pytest_cmdline_main(config):
if config.option.distload: if config.option.distload:
config.option.dist = "load" config.option.dist = "load"
val = config.getvalue val = config.getvalue
if not val("collectonly"): if not val("collectonly") and val("dist") != "no" and usepdb:
if val("dist") != "no": raise pytest.UsageError(
if usepdb: "--pdb is incompatible with distributing tests; try using -n0 or -nauto."
raise pytest.UsageError( ) # noqa: E501
"--pdb is incompatible with distributing tests; try using -n0 or -nauto."
) # noqa: E501
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
# 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.
""" """
return get_xdist_worker_id(request)
@pytest.fixture(scope="session")
def testrun_uid(request):
"""Return the unique id of the current test."""
if hasattr(request.config, "workerinput"): if hasattr(request.config, "workerinput"):
return request.config.workerinput["workerid"] return request.config.workerinput["testrunuid"]
else: else:
return "master" return uuid.uuid4().hex

View File

@@ -11,17 +11,17 @@ 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", "?")
self.testrunuid = config.workerinput["testrunuid"]
self.log = py.log.Producer("worker-%s" % self.workerid) self.log = py.log.Producer("worker-%s" % self.workerid)
if not config.option.debug: if not config.option.debug:
py.log.setconsumer(self.log._keywords, None) py.log.setconsumer(self.log._keywords, None)
@@ -100,11 +100,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(
@@ -112,6 +109,7 @@ class WorkerInteractor(object):
) )
data["item_index"] = self.item_index data["item_index"] = self.item_index
data["worker_id"] = self.workerid data["worker_id"] = self.workerid
data["testrun_uid"] = self.testrunuid
assert self.session.items[self.item_index].nodeid == report.nodeid assert self.session.items[self.item_index].nodeid == report.nodeid
self.sendevent("testreport", data=data) self.sendevent("testreport", data=data)
@@ -123,31 +121,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_captured hook was introduced in pytest 3.8
if 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):
@@ -238,6 +219,7 @@ if __name__ == "__channelexec__":
importpath + os.pathsep + os.environ.get("PYTHONPATH", "") importpath + os.pathsep + os.environ.get("PYTHONPATH", "")
) )
os.environ["PYTEST_XDIST_TESTRUNUID"] = workerinput["testrunuid"]
os.environ["PYTEST_XDIST_WORKER"] = workerinput["workerid"] os.environ["PYTEST_XDIST_WORKER"] = workerinput["workerid"]
os.environ["PYTEST_XDIST_WORKER_COUNT"] = str(workerinput["workercount"]) os.environ["PYTEST_XDIST_WORKER_COUNT"] = str(workerinput["workercount"])
@@ -251,8 +233,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
@@ -358,12 +358,12 @@ class LoadScopeScheduling(object):
extra_nodes = len(self.nodes) - len(self.workqueue) extra_nodes = len(self.nodes) - len(self.workqueue)
if extra_nodes > 0: if extra_nodes > 0:
self.log("Shuting down {0} nodes".format(extra_nodes)) self.log("Shuting down {} nodes".format(extra_nodes))
for _ in range(extra_nodes): for _ in range(extra_nodes):
unused_node, assigned = self.assigned_work.popitem(last=True) unused_node, assigned = self.assigned_work.popitem(last=True)
self.log("Shuting down unused node {0}".format(unused_node)) self.log("Shuting down unused node {}".format(unused_node))
unused_node.shutdown() unused_node.shutdown()
# Assign initial workload # Assign initial workload

View File

@@ -1,8 +1,8 @@
from __future__ import print_function
import fnmatch import fnmatch
import os import os
import re import re
import sys import sys
import uuid
import py import py
import pytest import pytest
@@ -28,13 +28,16 @@ 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", "*~"]
def __init__(self, config, specs=None, defaultchdir="pyexecnetcache"): def __init__(self, config, specs=None, defaultchdir="pyexecnetcache"):
self.config = config self.config = config
self.trace = self.config.trace.get("nodemanager") self.trace = self.config.trace.get("nodemanager")
self.testrunuid = self.config.getoption("testrunuid")
if self.testrunuid is None:
self.testrunuid = uuid.uuid4().hex
self.group = execnet.Group() self.group = execnet.Group()
if specs is None: if specs is None:
specs = self._getxspecs() specs = self._getxspecs()
@@ -59,10 +62,7 @@ class NodeManager(object):
def setup_nodes(self, putevent): def setup_nodes(self, putevent):
self.config.hook.pytest_xdist_setupnodes(config=self.config, specs=self.specs) self.config.hook.pytest_xdist_setupnodes(config=self.config, specs=self.specs)
self.trace("setting up nodes") self.trace("setting up nodes")
nodes = [] return [self.setup_node(spec, putevent) for spec in self.specs]
for spec in self.specs:
nodes.append(self.setup_node(spec, putevent))
return nodes
def setup_node(self, spec, putevent): def setup_node(self, spec, putevent):
gw = self.group.makegateway(spec) gw = self.group.makegateway(spec)
@@ -89,8 +89,16 @@ class NodeManager(object):
import pytest import pytest
import _pytest import _pytest
pytestpath = pytest.__file__.rstrip("co") def get_dir(p):
pytestdir = py.path.local(_pytest.__file__).dirpath() """Return the directory path if p is a package or the path to the .py file otherwise."""
stripped = p.rstrip("co")
if os.path.basename(stripped) == "__init__.py":
return os.path.dirname(p)
else:
return stripped
pytestpath = get_dir(pytest.__file__)
pytestdir = get_dir(_pytest.__file__)
config = self.config config = self.config
candidates = [py._pydir, pytestpath, pytestdir] candidates = [py._pydir, pytestpath, pytestdir]
candidates += config.option.rsyncdir candidates += config.option.rsyncdir
@@ -101,7 +109,7 @@ class NodeManager(object):
for root in candidates: for root in candidates:
root = py.path.local(root).realpath() root = py.path.local(root).realpath()
if not root.check(): if not root.check():
raise pytest.UsageError("rsyncdir doesn't exist: %r" % (root,)) raise pytest.UsageError("rsyncdir doesn't exist: {!r}".format(root))
if root not in roots: if root not in roots:
roots.append(root) roots.append(root)
return roots return roots
@@ -114,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):
@@ -154,12 +162,11 @@ class HostRSync(execnet.RSync):
def __init__(self, sourcedir, *args, **kwargs): def __init__(self, sourcedir, *args, **kwargs):
self._synced = {} self._synced = {}
self._ignores = []
ignores = kwargs.pop("ignores", None) or [] ignores = kwargs.pop("ignores", None) or []
for x in ignores: self._ignores = [
x = getattr(x, "strpath", x) re.compile(fnmatch.translate(getattr(x, "strpath", x))) for x in ignores
self._ignores.append(re.compile(fnmatch.translate(x))) ]
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)
@@ -171,15 +178,13 @@ 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("%s:%s <= %s" % (gateway.spec, remotepath, path)) print("{}:{} <= {}".format(gateway.spec, remotepath, path))
def make_reltoroot(roots, args): def make_reltoroot(roots, args):
@@ -198,12 +203,12 @@ def make_reltoroot(roots, args):
parts[0] = root.basename + "/" + x parts[0] = root.basename + "/" + x
break break
else: else:
raise ValueError("arg %s not relative to an rsync root" % (arg,)) raise ValueError("arg {} not relative to an rsync root".format(arg))
result.append(splitcode.join(parts)) result.append(splitcode.join(parts))
return result return result
class WorkerController(object): class WorkerController:
ENDMARK = -1 ENDMARK = -1
class RemoteHook: class RemoteHook:
@@ -220,12 +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, "testrunuid": nodemanager.testrunuid,
"slavecount": len(nodemanager.specs),
"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)
@@ -233,7 +235,7 @@ class WorkerController(object):
py.log.setconsumer(self.log._keywords, None) py.log.setconsumer(self.log._keywords, None)
def __repr__(self): def __repr__(self):
return "<%s %s>" % (self.__class__.__name__, self.gateway.id) return "<{} {}>".format(self.__class__.__name__, self.gateway.id)
@property @property
def shutting_down(self): def shutting_down(self):
@@ -287,17 +289,17 @@ 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
def sendcommand(self, name, **kwargs): 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 %s(**%s)" % (name, kwargs)) self.log("sending command {}(**{})".format(name, kwargs))
self.channel.send((name, kwargs)) self.channel.send((name, kwargs))
def notify_inproc(self, eventname, **kwargs): def notify_inproc(self, eventname, **kwargs):
self.log("queuing %s(**%s)" % (eventname, kwargs)) self.log("queuing {}(**{})".format(eventname, kwargs))
self.putevent((eventname, kwargs)) self.putevent((eventname, kwargs))
def process_from_remote(self, eventcall): # noqa too complex def process_from_remote(self, eventcall): # noqa too complex
@@ -319,12 +321,12 @@ class WorkerController(object):
return return
eventname, kwargs = eventcall eventname, kwargs = eventcall
if eventname in ("collectionstart",): if eventname in ("collectionstart",):
self.log("ignoring %s(%s)" % (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 == "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)
@@ -358,19 +360,26 @@ class WorkerController(object):
when=kwargs["when"], when=kwargs["when"],
item=kwargs["item"], item=kwargs["item"],
) )
elif eventname == "warning_recorded":
warning_message = unserialize_warning_message(
kwargs["warning_message_data"]
)
self.notify_inproc(
eventname,
warning_message=warning_message,
when=kwargs["when"],
nodeid=kwargs["nodeid"],
location=kwargs["location"],
)
else: else:
raise ValueError("unknown event: %s" % (eventname,)) raise ValueError("unknown event: {}".format(eventname))
except KeyboardInterrupt: except KeyboardInterrupt:
# should not land in receiver-thread # should not land in receiver-thread
raise raise
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:
@@ -242,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(
""" """
@@ -328,7 +311,7 @@ class TestDistribution:
time.sleep(10) time.sleep(10)
""" """
) )
child = testdir.spawn_pytest("-n1 -v") child = testdir.spawn_pytest("-n1 -v", expect_timeout=30.0)
child.expect(".*test_sleep.*") child.expect(".*test_sleep.*")
child.kill(2) # keyboard interrupt child.kill(2) # keyboard interrupt
child.expect(".*KeyboardInterrupt.*") child.expect(".*KeyboardInterrupt.*")
@@ -590,16 +573,13 @@ def test_fixture_teardown_failure(testdir):
assert result.ret assert result.ret
def test_config_initialization(testdir, 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": """
def test_1(): pass def test_1(request):
assert request.config.option.verbose == 2
""" """
} }
) )
@@ -610,8 +590,10 @@ def test_config_initialization(testdir, pytestconfig):
testpaths=dir_a testpaths=dir_a
""", """,
) )
monkeypatch.setenv("PYTEST_ADDOPTS", "-v")
result = testdir.runpytest("-n2", "-c", "myconfig.ini", "-v") result = testdir.runpytest("-n2", "-c", "myconfig.ini", "-v")
result.stdout.fnmatch_lines(["dir_a/test_foo.py::test_1*"]) 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"]) @pytest.mark.parametrize("when", ["setup", "call", "teardown"])
@@ -689,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(
""" """
@@ -792,31 +748,45 @@ 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*"])
def test_warning_captured_deprecated_in_pytest_6(self, testdir):
"""
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(
"""
def pytest_warning_captured():
assert False, "this hook should not be called in this version"
"""
)
testdir.makepyfile(
"""
import warnings
def test():
warnings.warn("custom warning")
"""
)
result = testdir.runpytest("-n1")
result.stdout.fnmatch_lines(["* 1 passed in *"])
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"])
def test_custom_subclass(self, testdir, n): def test_custom_subclass(self, testdir, n):
"""Check that warning subclasses that don't honor the args attribute don't break """Check that warning subclasses that don't honor the args attribute don't break
@@ -863,9 +833,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
@@ -1066,6 +1033,29 @@ def test_worker_id_fixture(testdir, n):
assert worker_ids == {"gw0", "gw1"} assert worker_ids == {"gw0", "gw1"}
@pytest.mark.parametrize("n", [0, 2])
def test_testrun_uid_fixture(testdir, n):
import glob
f = testdir.makepyfile(
"""
import pytest
@pytest.mark.parametrize("run_num", range(2))
def test_testrun_uid1(testrun_uid, run_num):
with open("testrun_uid%s.txt" % run_num, "w") as f:
f.write(testrun_uid)
"""
)
result = testdir.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"))):
with open(fname) as f:
testrun_uids.add(f.read().strip())
assert len(testrun_uids) == 1
assert len(testrun_uids.pop()) == 32
@pytest.mark.parametrize("tb", ["auto", "long", "short", "no", "line", "native"]) @pytest.mark.parametrize("tb", ["auto", "long", "short", "no", "line", "native"])
def test_error_report_styles(testdir, tb): def test_error_report_styles(testdir, tb):
testdir.makepyfile( testdir.makepyfile(
@@ -1358,7 +1348,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
""", """,
@@ -1377,3 +1367,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):
@@ -22,12 +11,16 @@ def _divert_atexit(request, monkeypatch):
finalizers = [] finalizers = []
def finish(): def fake_register(func, *args, **kwargs):
while finalizers: finalizers.append((func, args, kwargs))
finalizers.pop()()
monkeypatch.setattr(atexit, "register", finalizers.append) monkeypatch.setattr(atexit, "register", fake_register)
request.addfinalizer(finish)
yield
while finalizers:
func, args, kwargs = finalizers.pop()
func(*args, **kwargs)
def pytest_addoption(parser): def pytest_addoption(parser):

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

@@ -284,7 +284,7 @@ class TestFunctional:
) )
# p = testdir.mkdir("sub").join(p1.basename) # p = testdir.mkdir("sub").join(p1.basename)
# p1.move(p) # p1.move(p)
child = testdir.spawn_pytest("-f %s --traceconfig" % p) child = testdir.spawn_pytest("-f %s --traceconfig" % p, expect_timeout=30.0)
child.expect("def test_one") child.expect("def test_one")
child.expect("x == 1") child.expect("x == 1")
child.expect("1 failed") child.expect("1 failed")
@@ -311,7 +311,7 @@ class TestFunctional:
pass pass
""" """
) )
child = testdir.spawn_pytest("-f %s" % p) child = testdir.spawn_pytest("-f %s" % p, expect_timeout=30.0)
child.expect("1 xpass") child.expect("1 xpass")
# child.expect("### LOOPONFAILING ####") # child.expect("### LOOPONFAILING ####")
child.expect("waiting for changes") child.expect("waiting for changes")

View File

@@ -15,7 +15,7 @@ class TestHooks:
def test_runtest_logreport(self, testdir): def test_runtest_logreport(self, testdir):
"""Test that log reports from pytest_runtest_logreport when running """Test that log reports from pytest_runtest_logreport when running
with xdist contain "node", "nodeid" and "worker_id" attributes. (#8) with xdist contain "node", "nodeid", "worker_id", and "testrun_uid" attributes. (#8)
""" """
testdir.makeconftest( testdir.makeconftest(
""" """
@@ -23,20 +23,24 @@ class TestHooks:
if hasattr(report, 'node'): if hasattr(report, 'node'):
if report.when == "call": if report.when == "call":
workerid = report.node.workerinput['workerid'] workerid = report.node.workerinput['workerid']
testrunuid = report.node.workerinput['testrunuid']
if workerid != report.worker_id: if workerid != report.worker_id:
print("HOOK: Worker id mismatch: %s %s" print("HOOK: Worker id mismatch: %s %s"
% (workerid, report.worker_id)) % (workerid, report.worker_id))
elif testrunuid != report.testrun_uid:
print("HOOK: Testrun uid mismatch: %s %s"
% (testrunuid, report.testrun_uid))
else: else:
print("HOOK: %s %s" print("HOOK: %s %s %s"
% (report.nodeid, report.worker_id)) % (report.nodeid, report.worker_id, report.testrun_uid))
""" """
) )
res = testdir.runpytest("-n1", "-s") res = testdir.runpytest("-n1", "-s")
res.stdout.fnmatch_lines( res.stdout.fnmatch_lines(
[ [
"*HOOK: test_runtest_logreport.py::test_a gw0*", "*HOOK: test_runtest_logreport.py::test_a gw0 *",
"*HOOK: test_runtest_logreport.py::test_b gw0*", "*HOOK: test_runtest_logreport.py::test_b gw0 *",
"*HOOK: test_runtest_logreport.py::test_c gw0*", "*HOOK: test_runtest_logreport.py::test_c gw0 *",
"*3 passed*", "*3 passed*",
] ]
) )

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,19 +60,48 @@ 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")
check_options(config) check_options(config)
assert config.getoption("usepdb") assert config.getoption("usepdb")
assert config.getoption("numprocesses") == 0 assert config.getoption("numprocesses") == 0
assert config.getoption("dist") == "no"
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
@@ -95,6 +133,18 @@ def test_dsession_with_collect_only(testdir):
assert not config.pluginmanager.hasplugin("dsession") assert not config.pluginmanager.hasplugin("dsession")
def test_testrunuid_provided(testdir):
config = testdir.parseconfigure("--testrunuid", "test123", "--tx=popen")
nm = NodeManager(config)
assert nm.testrunuid == "test123"
def test_testrunuid_generated(testdir):
config = testdir.parseconfigure("--tx=popen")
nm = NodeManager(config)
assert len(nm.testrunuid) == 32
class TestDistOptions: class TestDistOptions:
def test_getxspecs(self, testdir): def test_getxspecs(self, testdir):
config = testdir.parseconfigure("--tx=popen", "--tx", "ssh=xyz") config = testdir.parseconfigure("--tx=popen", "--tx", "ssh=xyz")

View File

@@ -2,12 +2,13 @@ import py
import pprint import pprint
import pytest import pytest
import sys import sys
import uuid
from xdist.workermanage import WorkerController 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
@@ -25,7 +26,7 @@ class EventCall:
self.name, self.kwargs = eventcall self.name, self.kwargs = eventcall
def __str__(self): def __str__(self):
return "<EventCall %s(**%s)>" % (self.name, self.kwargs) return "<EventCall {}(**{})>".format(self.name, self.kwargs)
class WorkerSetup: class WorkerSetup:
@@ -44,6 +45,7 @@ class WorkerSetup:
putevent = self.use_callback and self.events.put or None putevent = self.use_callback and self.events.put or None
class DummyMananger: class DummyMananger:
testrunuid = uuid.uuid4().hex
specs = [0, 1] specs = [0, 1]
self.slp = WorkerController(DummyMananger, self.gateway, config, putevent) self.slp = WorkerController(DummyMananger, self.gateway, config, putevent)
@@ -59,7 +61,7 @@ class WorkerSetup:
ev = EventCall(data) ev = EventCall(data)
if name is None or ev.name == name: if name is None or ev.name == name:
return ev return ev
print("skipping %s" % (ev,)) print("skipping {}".format(ev))
def sendcommand(self, name, **kwargs): def sendcommand(self, name, **kwargs):
self.slp.sendcommand(name, **kwargs) self.slp.sendcommand(name, **kwargs)
@@ -220,6 +222,7 @@ def test_remote_env_vars(testdir):
""" """
import os import os
def test(): def test():
assert len(os.environ['PYTEST_XDIST_TESTRUNUID']) == 32
assert os.environ['PYTEST_XDIST_WORKER'] in ('gw0', 'gw1') assert os.environ['PYTEST_XDIST_WORKER'] in ('gw0', 'gw1')
assert os.environ['PYTEST_XDIST_WORKER_COUNT'] == '2' assert os.environ['PYTEST_XDIST_WORKER_COUNT'] == '2'
""" """

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
@@ -168,7 +167,7 @@ class TestNodeManager:
# assert nodemanager.config.topdir == source == config.topdir # assert nodemanager.config.topdir == source == config.topdir
nodemanager.makegateways() nodemanager.makegateways()
nodemanager.rsync_roots() nodemanager.rsync_roots()
p, = nodemanager.gwmanager.multi_exec( (p,) = nodemanager.gwmanager.multi_exec(
"import os ; channel.send(os.getcwd())" "import os ; channel.send(os.getcwd())"
).receive_each() ).receive_each()
p = py.path.local(p) p = py.path.local(p)
@@ -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)
@@ -269,5 +287,5 @@ class TestNodeManager:
reprec = testdir.inline_run( reprec = testdir.inline_run(
"-d", "--rsyncdir=%s" % testdir.tmpdir, "--tx", specssh, testdir.tmpdir "-d", "--rsyncdir=%s" % testdir.tmpdir, "--tx", specssh, testdir.tmpdir
) )
rep, = reprec.getreports("pytest_runtest_logreport") (rep,) = reprec.getreports("pytest_runtest_logreport")
assert rep.passed assert rep.passed

21
tox.ini
View File

@@ -1,21 +1,28 @@
[tox] [tox]
envlist= envlist=
linting linting
py{27,34,35,36,37,38}-pytestlatest py{35,36,37,38,39}-pytestlatest
py38-pytest{master,features} py38-pytestmaster
py38-psutil
[testenv] [testenv]
passenv = USER USERNAME
extras = testing extras = testing
deps = deps =
pytestlatest: pytest pytestlatest: pytest
pytestmaster: git+https://github.com/pytest-dev/pytest.git@master pytestmaster: git+https://github.com/pytest-dev/pytest.git@master
pytestfeatures: git+https://github.com/pytest-dev/pytest.git@features
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
deps = deps =
pre-commit pre-commit
@@ -29,9 +36,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