Compare commits

..

54 Commits

Author SHA1 Message Date
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
Bruno Oliveira
1f139a85d7 Release 1.31.0 2019-12-19 22:28:21 -03:00
Bruno Oliveira
1e284d31d0 Move 1.30.0 changelog to the top (by mistake?) 2019-12-19 22:25:42 -03:00
Ionel Cristian Mărieș
038a59eebc Fix regression with custom arguments being dropped in non-local… (#491)
* No idea what I'm doing.

* Extend integration test to cover ac3f6a78c7401aba9b62c2c67f280a129ae84eaf.

* Remove useless output to stop failing other tests.

* Reformat.

* Upgrade virtualenv to see if it fixes py34 builds

* Add CHANGELOG entry

* Make foobarplugin part of the test only

* Fix language_version in pre-commit for rst

* Fix test in Python 2

Co-authored-by: Bruno Oliveira <nicoddemus@gmail.com>
2019-12-19 21:00:02 -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
Bruno Oliveira
66e74c8eb3 Add support for Python 3.8 (#486)
Add support for Python 3.8
2019-11-22 09:03:27 -03:00
Hugo
7eb08599c1 Add news file 2019-11-09 17:02:35 +02:00
Hugo
94d231c462 Either appveyor.yml or .appveyor.yml is allowed, let's hide with other dotfiles 2019-11-09 16:50:34 +02:00
Hugo
20532ec93c Add support for Python 3.8 2019-11-09 16:45:40 +02:00
Bruno Oliveira
cd26963678 Fix typo 2019-11-04 16:42:28 -03:00
Bruno Oliveira
2c6bc135d9 Add an example on how to share session fixture data to README (#483)
Add an example on how to share session fixture data to README
2019-11-04 16:38:37 -03:00
Bruno Oliveira
0c53761e95 Fix tests for pytest features branch 2019-11-04 16:21:13 -03:00
Bruno Oliveira
f7e7d87ecf Add an example on how to share session fixture data to README
As discussed in #385
2019-11-02 19:43:36 -03:00
Ronny Pfannschmidt
79dd52b755 Merge pull request #470 from RonnyPfannschmidt/release-1.30.0
prepare 1.30.0 release
2019-10-01 17:44:18 +02:00
Ronny Pfannschmidt
30b540cd1f prepare 1.30.0 release 2019-10-01 10:35:55 +02:00
Ronny Pfannschmidt
79c14b2320 Merge pull request #467 from TBoshoven/fix-no-terminal
Fix issues related to running xdist with the terminal plugin disabled
2019-09-13 14:45:50 +02:00
Bruno Oliveira
ddc52f1e1a Ensure pytest ends successfully in test_without_terminal_plugin 2019-09-13 09:17:28 -03:00
Tom Boshoven
8cbbbe1a2a Add another no:terminal fix and add a test. 2019-09-12 10:32:20 -04:00
Tom Boshoven
0b0de014ec Add changelog. 2019-09-11 19:56:17 -04:00
Tom Boshoven
32a29138fc Fix issues related to running xdist with the terminal plugin disabled
This fixes an issue where the pytest plugin manager returns None if a plugin is not loaded instead of raising an error.
It also makes terminal optional in plugin code.
2019-09-11 19:47:16 -04:00
Bruno Oliveira
9f9b707481 Remove pytest 2.8 compatibility code (#451)
Remove pytest 2.8 compatibility code
2019-07-11 08:11:37 -03:00
Bruno Oliveira
4642f16e53 Merge pull request #448 from nicoddemus/config-init-test
Use Config.invocation_params for consistent worker initialization
2019-07-11 07:47:11 -03:00
Bruno Oliveira
2ca058e38a Remove pytest 2.8 compatibility code 2019-07-11 07:46:53 -03:00
Bruno Oliveira
953a3f0b4d Use Config.invocation_params for consistent worker initialization
Decided to keep the old way still working for now.

Fix #6
Fix #445
2019-07-10 19:59:12 -03:00
Bruno Oliveira
a19d74d5fe Merge pull request #443 from nicoddemus/release-1.29.0
Release 1.29.0
2019-06-14 17:12:50 -03:00
Bruno Oliveira
7daba1c618 Release 1.29.0 2019-06-14 12:30:51 -03:00
Bruno Oliveira
7ebf3ecee2 Compatibility for IntEnum change in upcoming pytest 5.0 (#442)
Compatibility for IntEnum change in upcoming pytest 5.0
2019-06-14 12:28:47 -03:00
Bruno Oliveira
fb40056950 Compatibility for IntEnum change in upcoming pytest 5.0
Related to pytest-dev/pytest#5420
2019-06-14 11:23:29 -03:00
Bruno Oliveira
407d6e16e2 Do not show internal error when running out of workers (#437)
Do not show internal error when running out of workers
2019-06-07 10:15:28 -03:00
Bruno Oliveira
956c79700f Do not show internal error when running out of workers
Also show the reason of the session being interrupted
in the warnings summary

Fix #435
2019-06-07 09:35:22 -03:00
Bruno Oliveira
e165c2f3fb Use "nodes * 4" as default for --max-worker-restart (#436)
Use "nodes * 4" as default for --max-worker-restart
2019-06-07 09:19:24 -03:00
Bruno Oliveira
bbc5416037 Use "nodes * 4" as default for --max-worker-restart
Fix #226
2019-06-06 19:53:37 -03:00
Bruno Oliveira
a630e55887 Merge pull request #439 from nicoddemus/src-layout
Adopt 'src' layout and add 'testing' extras
2019-06-06 19:52:17 -03:00
Bruno Oliveira
7d41db3685 Adopt 'src' layout and add 'testing' extras 2019-06-06 19:35:13 -03:00
Bruno Oliveira
cdf422dec8 Improve CI: drop testing pytest master on py27, use py37 (#438)
Improve CI: drop testing pytest master on py27, use py37
2019-06-06 18:31:46 -03:00
Bruno Oliveira
abec5e60a2 Improve CI: drop testing pytest master on py27, use py37
* Use Python 3.7 as preferred Python 3 interpreter
* Do not test pytest `master` and `features` branches on
  Python 2.7, as those branches no longer support Python 2.7/3.4
2019-06-06 17:12:27 -03:00
Bruno Oliveira
9fcf8fa636 Merge pull request #430 from nicoddemus/release-1.28.0
Release 1.28.0
2019-04-03 18:12:01 -03:00
28 changed files with 508 additions and 153 deletions

View File

@@ -1,23 +1,20 @@
environment: environment:
matrix: matrix:
- TOXENV: "py27-pytestlatest" - 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: "py27-pytestmaster" - TOXENV: "py38-pytestlatest"
- TOXENV: "py36-pytestmaster" - TOXENV: "py38-pytestmaster"
- TOXENV: "py27-pytestfeatures"
- TOXENV: "py36-pytestfeatures"
install: install:
- C:\Python37\python -m pip install -U pip - C:\Python38\python -m pip install -U pip setuptools virtualenv
- C:\Python37\python -m pip install -U tox setuptools_scm - C:\Python38\python -m pip install -U tox setuptools_scm
build: false # Not a C# project, build stuff at the test step instead. build: false # Not a C# project, build stuff at the test step instead.
test_script: test_script:
- C:\Python37\python -m tox - C:\Python38\python -m tox
# We don't deploy anything on tags with AppVeyor, we use Travis instead, so we # We don't deploy anything on tags with AppVeyor, we use Travis instead, so we
# might as well save resources # might as well save resources

2
.gitignore vendored
View File

@@ -23,7 +23,7 @@ include/
lib/ lib/
bin/ bin/
env/ env/
xdist/_version.py* src/xdist/_version.py*
pytest_xdist.egg-info pytest_xdist.egg-info
issue/ issue/
3rdparty/ 3rdparty/

View File

@@ -1,12 +1,12 @@
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]
language_version: python3.6 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
@@ -21,4 +21,4 @@ repos:
files: ^(CHANGELOG.rst|HOWTORELEASE.rst|README.rst|changelog/.*)$ files: ^(CHANGELOG.rst|HOWTORELEASE.rst|README.rst|changelog/.*)$
language: python language: python
additional_dependencies: [pygments, restructuredtext_lint] additional_dependencies: [pygments, restructuredtext_lint]
python_version: python3.6 language_version: python3.7

View File

@@ -12,7 +12,7 @@ notifications:
- pytest-commit@python.org - pytest-commit@python.org
install: install:
- pip install -U pip - pip install -U pip setuptools
- pip install tox setuptools_scm - pip install tox setuptools_scm
script: tox script: tox
@@ -25,34 +25,28 @@ stages:
jobs: jobs:
include: include:
- stage: baseline - stage: baseline
python: '3.6' python: '3.7'
env: TOXENV=linting env: TOXENV=linting
cache: cache:
directories: directories:
- $HOME/.cache/pre-commit - $HOME/.cache/pre-commit
- python: '3.6' - python: '3.8'
env: TOXENV=py36-pytestlatest env: TOXENV=py38-pytestlatest
- python: '2.7' - python: '2.7'
env: TOXENV=py27-pytestlatest 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"
env: TOXENV=py36-pytestlatest
- python: "3.7" - python: "3.7"
env: TOXENV=py37-pytestlatest env: TOXENV=py37-pytestlatest
- python: "2.7" - python: "3.8"
env: TOXENV=py27-pytestmaster env: TOXENV=py38-pytestmaster
- python: "2.7"
env: TOXENV=py27-pytestfeatures
- python: "3.6"
env: TOXENV=py36-pytestmaster
- python: "3.6"
env: TOXENV=py36-pytestfeatures
- stage: deploy - stage: deploy
python: '3.6' python: '3.8'
env: env:
install: pip install -U setuptools setuptools_scm install: pip install -U setuptools setuptools_scm
script: skip script: skip

View File

@@ -1,3 +1,79 @@
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)
================================
Features
--------
- `#486 <https://github.com/pytest-dev/pytest-xdist/issues/486>`_: Add support for Python 3.8.
Bug Fixes
---------
- `#491 <https://github.com/pytest-dev/pytest-xdist/issues/491>`_: Fix regression that caused custom plugin command-line arguments to be discarded when using ``--tx`` mode.
pytest-xdist 1.30.0 (2019-10-01)
================================
Features
--------
- `#448 <https://github.com/pytest-dev/pytest-xdist/issues/448>`_: Initialization between workers and master nodes is now more consistent, which fixes a number of
long-standing issues related to startup with the ``-c`` option.
Issues:
* `#6 <https://github.com/pytest-dev/pytest-xdist/issues/6>`__: Poor interaction between ``-n#`` and ``-c X.cfg``
* `#445 <https://github.com/pytest-dev/pytest-xdist/issues/445>`__: pytest-xdist is not reporting the same nodeid as pytest does
This however only works with **pytest 5.1 or later**, as it required changes in pytest itself.
Bug Fixes
---------
- `#467 <https://github.com/pytest-dev/pytest-xdist/issues/467>`_: Fix crash issues related to running xdist with the terminal plugin disabled.
pytest-xdist 1.29.0 (2019-06-14)
================================
Features
--------
- `#226 <https://github.com/pytest-dev/pytest-xdist/issues/226>`_: ``--max-worker-restart`` now assumes a more reasonable value (4 times the number of
nodes) when not given explicitly. This prevents test suites from running forever when the suite crashes during collection.
- `#435 <https://github.com/pytest-dev/pytest-xdist/issues/435>`_: When the test session is interrupted due to running out of workers, the reason is shown in the test summary
for easier viewing.
- `#442 <https://github.com/pytest-dev/pytest-xdist/issues/442>`_: Compatibility fix for upcoming pytest 5.0: ``session.exitstatus`` is now an ``IntEnum`` object.
Bug Fixes
---------
- `#435 <https://github.com/pytest-dev/pytest-xdist/issues/435>`_: No longer show an internal error when we run out of workers due to crashes.
pytest-xdist 1.28.0 (2019-04-02) pytest-xdist 1.28.0 (2019-04-02)
================================ ================================
@@ -380,17 +456,6 @@ Improved Documentation
- Added ``HOWTORELEASE`` documentation. (#155) - Added ``HOWTORELEASE`` documentation. (#155)
..
You should *NOT* be adding new change log entries to this file, this
file is managed by towncrier. You *may* edit previous change logs to
fix problems like typo corrections or such.
To add a new change log entry, please see
https://pip.pypa.io/en/latest/development/#adding-a-news-entry
We named the news folder ``changelog``
.. towncrier release notes start
1.17.0 1.17.0
------ ------

View File

@@ -95,6 +95,59 @@ any guaranteed order, but you can control this with these options:
in version ``1.21``. in version ``1.21``.
Making session-scoped fixtures execute only once
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
``pytest-xdist`` is designed so that each worker process will perform its own collection and execute
a subset of all tests. This means that tests in different processes requesting a high-level
scoped fixture (for example ``session``) will execute the fixture code more than once, which
breaks expectations and might be undesired in certain situations.
While ``pytest-xdist`` does not have a builtin support for ensuring a session-scoped fixture is
executed exactly once, this can be achieved by using a lock file for inter-process communication.
The example below needs to execute the fixture ``session_data`` only once (because it is
resource intensive, or needs to execute only once to define configuration options, etc), so it makes
use of a `FileLock <https://pypi.org/project/filelock/>`_ to produce the fixture data only once
when the first process requests the fixture, while the other processes will then read
the data from a file.
Here is the code:
.. code-block:: python
import json
import pytest
from filelock import FileLock
@pytest.fixture(scope="session")
def session_data(tmp_path_factory, worker_id):
if not worker_id:
# not executing in with multiple workers, just produce the data and let
# pytest's fixture caching do its job
return produce_expensive_data()
# get the temp directory shared by all workers
root_tmp_dir = tmp_path_factory.getbasetemp().parent
fn = root_tmp_dir / "data.json"
with FileLock(str(fn) + ".lock"):
if fn.is_file():
data = json.loads(fn.read_text())
else:
data = produce_expensive_data()
fn.write_text(json.dumps(data))
return data
The example above can also be use in cases a fixture needs to execute exactly once per test session, like
initializing a database service and populating initial tables.
This technique might not work for every case, but should be a starting point for many situations
where executing a high-scope fixture exactly once is important.
Running tests in a Python subprocess Running tests in a Python subprocess
------------------------------------ ------------------------------------
@@ -223,8 +276,43 @@ 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
----------------------------------------------------- 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

@@ -8,7 +8,7 @@ with open("README.rst") as f:
setup( setup(
name="pytest-xdist", name="pytest-xdist",
use_scm_version={"write_to": "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,
@@ -17,12 +17,14 @@ setup(
author_email="pytest-dev@python.org,holger@merlinux.eu", author_email="pytest-dev@python.org,holger@merlinux.eu",
url="https://github.com/pytest-dev/pytest-xdist", url="https://github.com/pytest-dev/pytest-xdist",
platforms=["linux", "osx", "win32"], platforms=["linux", "osx", "win32"],
packages=find_packages(exclude=["testing", "example"]), packages=find_packages(where="src"),
package_dir={"": "src"},
extras_require={"testing": ["filelock"]},
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=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*",
install_requires=install_requires, install_requires=install_requires,
setup_requires=["setuptools_scm"], setup_requires=["setuptools_scm"],
classifiers=[ classifiers=[
@@ -40,9 +42,9 @@ setup(
"Programming Language :: Python :: 2", "Programming Language :: Python :: 2",
"Programming Language :: Python :: 2.7", "Programming Language :: Python :: 2.7",
"Programming Language :: Python :: 3", "Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.4",
"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",
"Programming Language :: Python :: 3.8",
], ],
) )

View File

@@ -46,14 +46,11 @@ class DSession(object):
self._failed_collection_errors = {} self._failed_collection_errors = {}
self._active_nodes = set() self._active_nodes = set()
self._failed_nodes_count = 0 self._failed_nodes_count = 0
self._max_worker_restart = self.config.option.maxworkerrestart self._max_worker_restart = get_default_max_worker_restart(self.config)
if self._max_worker_restart is not None: # summary message to print at the end of the session
self._max_worker_restart = int(self._max_worker_restart) self._summary_report = None
try: self.terminal = config.pluginmanager.getplugin("terminalreporter")
self.terminal = config.pluginmanager.getplugin("terminalreporter") if self.terminal:
except KeyError:
self.terminal = None
else:
self.trdist = TerminalDistReporter(config) self.trdist = TerminalDistReporter(config)
config.pluginmanager.register(self.trdist, "terminaldistreporter") config.pluginmanager.register(self.trdist, "terminaldistreporter")
@@ -172,7 +169,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:
@@ -198,15 +195,23 @@ class DSession(object):
) )
if maximum_reached: if maximum_reached:
if self._max_worker_restart == 0: if self._max_worker_restart == 0:
msg = "Worker restarting disabled" msg = "worker {} crashed and worker restarting disabled".format(
node.gateway.id
)
else: else:
msg = "Maximum crashed workers reached: %d" % self._max_worker_restart msg = "maximum crashed workers reached: %d" % self._max_worker_restart
self.report_line(msg) self._summary_report = msg
self.report_line("\n" + msg)
self.triggershutdown()
else: else:
self.report_line("Replacing crashed worker %s" % node.gateway.id) self.report_line("\nreplacing crashed worker %s" % node.gateway.id)
self._clone_node(node) self._clone_node(node)
self._active_nodes.remove(node) self._active_nodes.remove(node)
def pytest_terminal_summary(self, terminalreporter):
if self.config.option.verbose >= 0 and self._summary_report:
terminalreporter.write_sep("=", "xdist: {}".format(self._summary_report))
def worker_collectionfinish(self, node, ids): def worker_collectionfinish(self, node, ids):
"""worker has finished test collection. """worker has finished test collection.
@@ -316,7 +321,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, "???"
) )
@@ -346,7 +351,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..."
@@ -381,19 +388,27 @@ 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 pytest_xdist_rsyncstart(self, source, gateways):
# targets = ",".join([gw.id for gw in gateways]) def get_default_max_worker_restart(config):
# msg = "[%s] rsyncing: %s" %(targets, source) """gets the default value of --max-worker-restart option if it is not provided.
# self.write_line(msg)
# def pytest_xdist_rsyncfinish(self, source, gateways): Use a reasonable default to avoid workers from restarting endlessly due to crashing collections (#226).
# targets = ", ".join(["[%s]" % gw.id for gw in gateways]) """
# self.write_line("rsyncfinish: %s -> %s" %(source, targets)) result = config.option.maxworkerrestart
if result is not None:
result = int(result)
elif config.option.numprocesses:
# if --max-worker-restart was not provided, use a reasonable default (#226)
result = config.option.numprocesses * 4
return result

View File

@@ -136,7 +136,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):

View File

@@ -1,4 +1,5 @@
import os import os
import uuid
import py import py
import pytest import pytest
@@ -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.",
@@ -154,11 +165,7 @@ def pytest_addoption(parser):
def pytest_addhooks(pluginmanager): def pytest_addhooks(pluginmanager):
from xdist import newhooks from xdist import newhooks
# avoid warnings with pytest-2.8 pluginmanager.add_hookspecs(newhooks)
method = getattr(pluginmanager, "add_hookspecs", None)
if method is None:
method = pluginmanager.addhooks
method(newhooks)
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
@@ -174,7 +181,8 @@ def pytest_configure(config):
session = DSession(config) session = DSession(config)
config.pluginmanager.register(session, "dsession") config.pluginmanager.register(session, "dsession")
tr = config.pluginmanager.getplugin("terminalreporter") tr = config.pluginmanager.getplugin("terminalreporter")
tr.showfspath = False if tr:
tr.showfspath = False
if config.getoption("boxed"): if config.getoption("boxed"):
config.option.forked = True config.option.forked = True
@@ -217,3 +225,12 @@ def worker_id(request):
return request.config.workerinput["workerid"] return request.config.workerinput["workerid"]
else: else:
return "master" return "master"
@pytest.fixture(scope="session")
def testrun_uid(request):
"""Return the unique id of the current test."""
if hasattr(request.config, "workerinput"):
return request.config.workerinput["testrunuid"]
else:
return uuid.uuid4().hex

View File

@@ -15,11 +15,14 @@ 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
class WorkerInteractor(object): class WorkerInteractor(object):
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)
@@ -41,7 +44,8 @@ class WorkerInteractor(object):
@pytest.hookimpl(hookwrapper=True) @pytest.hookimpl(hookwrapper=True)
def pytest_sessionfinish(self, exitstatus): def pytest_sessionfinish(self, exitstatus):
self.config.workeroutput["exitstatus"] = exitstatus # in pytest 5.0+, exitstatus is an IntEnum object
self.config.workeroutput["exitstatus"] = int(exitstatus)
yield yield
self.sendevent("workerfinished", workeroutput=self.config.workeroutput) self.sendevent("workerfinished", workeroutput=self.config.workeroutput)
@@ -109,6 +113,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)
@@ -210,18 +215,18 @@ def getinfodict():
def remote_initconfig(option_dict, args): def remote_initconfig(option_dict, args):
from _pytest.config import Config
option_dict["plugins"].append("no:terminal") option_dict["plugins"].append("no:terminal")
config = Config.fromdictargs(option_dict, args) return Config.fromdictargs(option_dict, args)
def setup_config(config, basetemp):
config.option.looponfail = False config.option.looponfail = False
config.option.usepdb = False config.option.usepdb = False
config.option.dist = "no" config.option.dist = "no"
config.option.distload = False config.option.distload = False
config.option.numprocesses = None config.option.numprocesses = None
config.option.maxprocesses = None config.option.maxprocesses = None
config.args = args config.option.basetemp = basetemp
return config
if __name__ == "__channelexec__": if __name__ == "__channelexec__":
@@ -235,10 +240,17 @@ 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"])
config = remote_initconfig(option_dict, args) if hasattr(Config, "InvocationParams"):
config = _prepareconfig(args, None)
else:
config = remote_initconfig(option_dict, args)
config.args = args
setup_config(config, option_dict.get("basetemp"))
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 = {}

View File

@@ -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

@@ -3,6 +3,7 @@ import fnmatch
import os import os
import re import re
import sys import sys
import uuid
import py import py
import pytest import pytest
@@ -35,6 +36,9 @@ class NodeManager(object):
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()
@@ -101,7 +105,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
@@ -112,7 +116,10 @@ class NodeManager(object):
ignores += self.config.option.rsyncignore ignores += self.config.option.rsyncignore
ignores += self.config.getini("rsyncignore") ignores += self.config.getini("rsyncignore")
return {"ignores": ignores, "verbose": self.config.option.verbose} return {
"ignores": ignores,
"verbose": getattr(self.config.option, "verbose", False),
}
def rsync(self, gateway, source, notify=None, verbose=False, ignores=None): def rsync(self, gateway, source, notify=None, verbose=False, ignores=None):
"""Perform rsync to remote hosts for node.""" """Perform rsync to remote hosts for node."""
@@ -176,7 +183,7 @@ class HostRSync(execnet.RSync):
if self._verbose: if self._verbose:
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):
@@ -186,13 +193,16 @@ def make_reltoroot(roots, args):
for arg in args: for arg in args:
parts = arg.split(splitcode) parts = arg.split(splitcode)
fspath = py.path.local(parts[0]) fspath = py.path.local(parts[0])
if not fspath.exists():
result.append(arg)
continue
for root in roots: for root in roots:
x = fspath.relto(root) x = fspath.relto(root)
if x or fspath == root: if x or fspath == root:
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
@@ -216,6 +226,7 @@ class WorkerController(object):
"workercount": len(nodemanager.specs), "workercount": len(nodemanager.specs),
"slaveid": gateway.id, "slaveid": gateway.id,
"slavecount": len(nodemanager.specs), "slavecount": len(nodemanager.specs),
"testrunuid": nodemanager.testrunuid,
"mainargv": sys.argv, "mainargv": sys.argv,
} }
# TODO: deprecated name, backward compatibility only. Remove it in future # TODO: deprecated name, backward compatibility only. Remove it in future
@@ -227,7 +238,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):
@@ -236,10 +247,14 @@ class WorkerController(object):
def setup(self): def setup(self):
self.log("setting up worker session") self.log("setting up worker session")
spec = self.gateway.spec spec = self.gateway.spec
args = self.config.args if hasattr(self.config, "invocation_params"):
args = [str(x) for x in self.config.invocation_params.args or ()]
option_dict = {}
else:
args = self.config.args
option_dict = vars(self.config.option)
if not spec.popen or spec.chdir: if not spec.popen or spec.chdir:
args = make_reltoroot(self.nodemanager.roots, args) args = make_reltoroot(self.nodemanager.roots, args)
option_dict = vars(self.config.option)
if spec.popen: if spec.popen:
name = "popen-%s" % self.gateway.id name = "popen-%s" % self.gateway.id
if hasattr(self.config, "_tmpdirhandler"): if hasattr(self.config, "_tmpdirhandler"):
@@ -283,11 +298,11 @@ class WorkerController(object):
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
@@ -309,7 +324,7 @@ 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":
@@ -349,7 +364,7 @@ class WorkerController(object):
item=kwargs["item"], item=kwargs["item"],
) )
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

View File

@@ -115,6 +115,9 @@ 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(
""" """
@@ -186,16 +189,47 @@ class TestDistribution:
) )
assert result.ret == 1 assert result.ret == 1
def test_distribution_rsyncdirs_example(self, testdir): @pytest.mark.xfail(reason="#527: Ignore Python 3.8 failure for the time being")
def test_distribution_rsyncdirs_example(self, testdir, monkeypatch):
# use a custom plugin that has a custom command-line option to ensure
# this is propagated to workers (see #491)
testdir.makepyfile(
**{
"myplugin/src/foobarplugin.py": """
from __future__ import print_function
import os
import sys
import pytest
def pytest_addoption(parser):
parser.addoption("--foobar", action="store", dest="foobar_opt")
@pytest.mark.tryfirst
def pytest_load_initial_conftests(early_config):
opt = early_config.known_args_namespace.foobar_opt
print("--foobar=%s active! [%s]" % (opt, os.getpid()), file=sys.stderr)
"""
}
)
assert (testdir.tmpdir / "myplugin/src/foobarplugin.py").check(file=1)
monkeypatch.setenv(
"PYTHONPATH", str(testdir.tmpdir / "myplugin/src"), prepend=os.pathsep
)
source = testdir.mkdir("source") source = testdir.mkdir("source")
dest = testdir.mkdir("dest") dest = testdir.mkdir("dest")
subdir = source.mkdir("example_pkg") subdir = source.mkdir("example_pkg")
subdir.ensure("__init__.py") subdir.ensure("__init__.py")
p = subdir.join("test_one.py") p = subdir.join("test_one.py")
p.write("def test_5():\n assert not __file__.startswith(%r)" % str(p)) p.write("def test_5():\n assert not __file__.startswith(%r)" % str(p))
result = testdir.runpytest( result = testdir.runpytest_subprocess(
"-v", "-v",
"-d", "-d",
"-s",
"-pfoobarplugin",
"--foobar=123",
"--dist=load",
"--rsyncdir=%(subdir)s" % locals(), "--rsyncdir=%(subdir)s" % locals(),
"--tx=popen//chdir=%(dest)s" % locals(), "--tx=popen//chdir=%(dest)s" % locals(),
p, p,
@@ -209,6 +243,7 @@ class TestDistribution:
"*1 passed*", "*1 passed*",
] ]
) )
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): def test_backward_compatibility_worker_terminology(self, testdir):
@@ -559,6 +594,30 @@ def test_fixture_teardown_failure(testdir):
assert result.ret assert result.ret
def test_config_initialization(testdir, pytestconfig):
"""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(
**{
"dir_a/test_foo.py": """
def test_1(): pass
"""
}
)
testdir.makefile(
".ini",
myconfig="""
[pytest]
testpaths=dir_a
""",
)
result = testdir.runpytest("-n2", "-c", "myconfig.ini", "-v")
result.stdout.fnmatch_lines(["dir_a/test_foo.py::test_1*"])
@pytest.mark.parametrize("when", ["setup", "call", "teardown"]) @pytest.mark.parametrize("when", ["setup", "call", "teardown"])
def test_crashing_item(testdir, when): def test_crashing_item(testdir, when):
"""Ensure crashing item is correctly reported during all testing stages""" """Ensure crashing item is correctly reported during all testing stages"""
@@ -760,7 +819,7 @@ class TestWarnings:
) )
) )
result = testdir.runpytest(n) result = testdir.runpytest(n)
result.stdout.fnmatch_lines(["*this is a warning*", "*1 passed, 1 warnings*"]) result.stdout.fnmatch_lines(["*this is a warning*", "*1 passed, 1 warning*"])
@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):
@@ -784,7 +843,7 @@ class TestWarnings:
) )
testdir.syspathinsert() testdir.syspathinsert()
result = testdir.runpytest(n) result = testdir.runpytest(n)
result.stdout.fnmatch_lines(["*MyWarning*", "*1 passed, 1 warnings*"]) result.stdout.fnmatch_lines(["*MyWarning*", "*1 passed, 1 warning*"])
@pytest.mark.parametrize("n", ["-n0", "-n1"]) @pytest.mark.parametrize("n", ["-n0", "-n1"])
def test_unserializable_arguments(self, testdir, n): def test_unserializable_arguments(self, testdir, n):
@@ -801,7 +860,7 @@ class TestWarnings:
) )
testdir.syspathinsert() testdir.syspathinsert()
result = testdir.runpytest(n) result = testdir.runpytest(n)
result.stdout.fnmatch_lines(["*UserWarning*foo.txt*", "*1 passed, 1 warnings*"]) result.stdout.fnmatch_lines(["*UserWarning*foo.txt*", "*1 passed, 1 warning*"])
@pytest.mark.parametrize("n", ["-n0", "-n1"]) @pytest.mark.parametrize("n", ["-n0", "-n1"])
def test_unserializable_warning_details(self, testdir, n): def test_unserializable_warning_details(self, testdir, n):
@@ -833,7 +892,7 @@ class TestWarnings:
testdir.syspathinsert() testdir.syspathinsert()
result = testdir.runpytest(n) result = testdir.runpytest(n)
result.stdout.fnmatch_lines( result.stdout.fnmatch_lines(
["*ResourceWarning*unclosed*", "*1 passed, 1 warnings*"] ["*ResourceWarning*unclosed*", "*1 passed, 1 warning*"]
) )
@@ -849,8 +908,8 @@ class TestNodeFailure:
res = testdir.runpytest(f, "-n1") res = testdir.runpytest(f, "-n1")
res.stdout.fnmatch_lines( res.stdout.fnmatch_lines(
[ [
"*Replacing crashed worker*", "replacing crashed worker gw*",
"*Worker*crashed while running*", "worker*crashed while running*",
"*1 failed*1 passed*", "*1 failed*1 passed*",
] ]
) )
@@ -868,8 +927,8 @@ class TestNodeFailure:
res = testdir.runpytest(f, "-n2") res = testdir.runpytest(f, "-n2")
res.stdout.fnmatch_lines( res.stdout.fnmatch_lines(
[ [
"*Replacing crashed worker*", "replacing crashed worker gw*",
"*Worker*crashed while running*", "worker*crashed while running*",
"*1 failed*3 passed*", "*1 failed*3 passed*",
] ]
) )
@@ -885,8 +944,8 @@ class TestNodeFailure:
res = testdir.runpytest(f, "--dist=each", "--tx=popen") res = testdir.runpytest(f, "--dist=each", "--tx=popen")
res.stdout.fnmatch_lines( res.stdout.fnmatch_lines(
[ [
"*Replacing crashed worker*", "replacing crashed worker gw*",
"*Worker*crashed while running*", "worker*crashed while running*",
"*1 failed*1 passed*", "*1 failed*1 passed*",
] ]
) )
@@ -922,14 +981,35 @@ class TestNodeFailure:
res = testdir.runpytest(f, "-n4", "--max-worker-restart=1") res = testdir.runpytest(f, "-n4", "--max-worker-restart=1")
res.stdout.fnmatch_lines( res.stdout.fnmatch_lines(
[ [
"*Replacing crashed worker*", "replacing crashed worker*",
"*Maximum crashed workers reached: 1*", "maximum crashed workers reached: 1*",
"*Worker*crashed while running*", "worker*crashed while running*",
"*Worker*crashed while running*", "worker*crashed while running*",
"*2 failed*2 passed*", "*2 failed*2 passed*",
] ]
) )
def test_max_worker_restart_tests_queued(self, testdir):
f = testdir.makepyfile(
"""
import os, pytest
@pytest.mark.parametrize('i', range(10))
def test(i): os._exit(1)
"""
)
res = testdir.runpytest(f, "-n2", "--max-worker-restart=3")
res.stdout.fnmatch_lines(
[
"replacing crashed worker*",
"maximum crashed workers reached: 3*",
"worker*crashed while running*",
"worker*crashed while running*",
"* xdist: maximum crashed workers reached: 3 *",
"* 4 failed in *",
]
)
assert "INTERNALERROR" not in res.stdout.str()
def test_max_worker_restart_die(self, testdir): def test_max_worker_restart_die(self, testdir):
f = testdir.makepyfile( f = testdir.makepyfile(
""" """
@@ -939,7 +1019,10 @@ class TestNodeFailure:
) )
res = testdir.runpytest(f, "-n4", "--max-worker-restart=0") res = testdir.runpytest(f, "-n4", "--max-worker-restart=0")
res.stdout.fnmatch_lines( res.stdout.fnmatch_lines(
["*Unexpectedly no active workers*", "*INTERNALERROR*"] [
"* xdist: worker gw* crashed and worker restarting disabled *",
"* no tests ran in *",
]
) )
def test_disable_restart(self, testdir): def test_disable_restart(self, testdir):
@@ -954,9 +1037,10 @@ class TestNodeFailure:
res = testdir.runpytest(f, "-n4", "--max-worker-restart=0") res = testdir.runpytest(f, "-n4", "--max-worker-restart=0")
res.stdout.fnmatch_lines( res.stdout.fnmatch_lines(
[ [
"*Worker restarting disabled*", "worker gw* crashed and worker restarting disabled",
"*Worker*crashed while running*", "*worker*crashed while running*",
"*1 failed*2 passed*", "* xdist: worker gw* crashed and worker restarting disabled *",
"* 1 failed, 2 passed in *",
] ]
) )
@@ -986,6 +1070,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(
@@ -1025,6 +1132,22 @@ def test_color_yes_collection_on_non_atty(testdir, request):
assert "gw0 C / gw1 C" not in result.stdout.str() assert "gw0 C / gw1 C" not in result.stdout.str()
def test_without_terminal_plugin(testdir, request):
"""
No output when terminal plugin is disabled
"""
testdir.makepyfile(
"""
def test_1():
pass
"""
)
result = testdir.runpytest("-p", "no:terminal", "-n2")
assert result.stdout.str() == ""
assert result.stderr.str() == ""
assert result.ret == 0
def test_internal_error_with_maxfail(testdir): def test_internal_error_with_maxfail(testdir):
""" """
Internal error when using --maxfail option (#62, #65). Internal error when using --maxfail option (#62, #65).

View File

@@ -1,20 +1,19 @@
import six
import py import py
import pytest import pytest
import execnet import execnet
@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_plugins = "pytester" pytest_plugins = "pytester"
# rsyncdirs = ['.', '../xdist', py.path.local(execnet.__file__).dirpath()] 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)
@@ -45,14 +44,6 @@ def specssh(request):
return getspecssh(request.config) return getspecssh(request.config)
@pytest.fixture
def testdir(testdir):
# pytest before 2.8 did not have a runpytest_subprocess
if not hasattr(testdir, "runpytest_subprocess"):
testdir.runpytest_subprocess = testdir.runpytest
return testdir
# configuration information for tests # configuration information for tests
def getgspecs(config): def getgspecs(config):
return [execnet.XSpec(spec) for spec in config.getvalueorskip("gspecs")] return [execnet.XSpec(spec) for spec in config.getvalueorskip("gspecs")]

View File

@@ -1,4 +1,4 @@
from xdist.dsession import DSession from xdist.dsession import DSession, get_default_max_worker_restart
from xdist.report import report_collection_diff from xdist.report import report_collection_diff
from xdist.scheduler import EachScheduling, LoadScheduling from xdist.scheduler import EachScheduling, LoadScheduling
@@ -268,6 +268,24 @@ def test_report_collection_diff_equal():
assert report_collection_diff(from_collection, to_collection, 1, 2) is None assert report_collection_diff(from_collection, to_collection, 1, 2) is None
def test_default_max_worker_restart():
class config:
class option:
maxworkerrestart = None
numprocesses = 0
assert get_default_max_worker_restart(config) is None
config.option.numprocesses = 2
assert get_default_max_worker_restart(config) == 8
config.option.maxworkerrestart = "1"
assert get_default_max_worker_restart(config) == 1
config.option.maxworkerrestart = "0"
assert get_default_max_worker_restart(config) == 0
def test_report_collection_diff_different(): def test_report_collection_diff_different():
"""Test reporting of different collections.""" """Test reporting of different collections."""
from_collection = ["aaa", "bbb", "ccc", "YYY"] from_collection = ["aaa", "bbb", "ccc", "YYY"]

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

@@ -95,6 +95,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,6 +2,7 @@ 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
@@ -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

@@ -168,7 +168,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)
@@ -269,5 +269,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

View File

@@ -1,16 +1,15 @@
[tox] [tox]
envlist= envlist=
linting linting
py{27,34,35,36,37}-pytestlatest py{27,35,36,37,38}-pytestlatest
py{27,36}-pytest{master,features} py38-pytestmaster
[testenv] [testenv]
passenv = USER USERNAME passenv = USER USERNAME
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
filelock
commands= commands=
pytest {posargs} pytest {posargs}
@@ -24,7 +23,7 @@ commands = pre-commit run --all-files --show-diff-on-failure
[testenv:release] [testenv:release]
changedir= changedir=
decription = do a release, required posarg of the version number decription = do a release, required posarg of the version number
basepython = python3.6 basepython = python3.7
skipsdist = True skipsdist = True
usedevelop = True usedevelop = True
passenv = * passenv = *