Compare commits

..

42 Commits

Author SHA1 Message Date
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
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
21 changed files with 328 additions and 93 deletions

View File

@@ -1,13 +1,11 @@
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: "py38-pytestlatest" - TOXENV: "py38-pytestlatest"
- TOXENV: "py38-pytestmaster" - TOXENV: "py38-pytestmaster"
- TOXENV: "py38-pytestfeatures"
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,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.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

View File

@@ -36,9 +36,7 @@ jobs:
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" - python: "3.6"
env: TOXENV=py36-pytestlatest env: TOXENV=py36-pytestlatest
@@ -46,8 +44,6 @@ jobs:
env: TOXENV=py37-pytestlatest env: TOXENV=py37-pytestlatest
- python: "3.8" - python: "3.8"
env: TOXENV=py38-pytestmaster env: TOXENV=py38-pytestmaster
- python: "3.8"
env: TOXENV=py38-pytestfeatures
- stage: deploy - stage: deploy
python: '3.8' python: '3.8'

View File

@@ -1,3 +1,53 @@
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)
================================ ================================

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

@@ -67,33 +67,35 @@ a checkout of the `pytest-xdist repository`_ ::
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
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@@ -276,8 +278,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

@@ -24,7 +24,7 @@ setup(
"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=[
@@ -42,7 +42,6 @@ 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",

View File

@@ -169,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:
@@ -277,10 +277,17 @@ class DSession(object):
self.config.hook.pytest_logwarning.call_historic(kwargs=kwargs) 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 +328,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, "???"
) )
@@ -351,7 +358,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 +395,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

@@ -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.",
@@ -180,7 +191,11 @@ def pytest_configure(config):
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 isinstance(config.option.numprocesses, AutoInt):
config.option.numprocesses = 0 if usepdb else int(config.option.numprocesses) if usepdb:
config.option.numprocesses = 0
config.option.dist = "no"
else:
config.option.numprocesses = int(config.option.numprocesses)
if config.option.numprocesses: if config.option.numprocesses:
if config.option.dist == "no": if config.option.dist == "no":
@@ -192,12 +207,10 @@ 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
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
@@ -214,3 +227,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

@@ -22,6 +22,7 @@ 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)
@@ -112,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)
@@ -137,8 +139,20 @@ class WorkerInteractor(object):
fslocation=str(fslocation), fslocation=str(fslocation),
) )
# the pytest_warning_recorded hook was introduced in pytest 6.0
if hasattr(_pytest.hookspec, "pytest_warning_recorded"):
def pytest_warning_recorded(self, warning_message, when, nodeid, location):
self.sendevent(
"warning_recorded",
warning_message_data=serialize_warning_message(warning_message),
when=when,
nodeid=nodeid,
location=location,
)
# the pytest_warning_captured hook was introduced in pytest 3.8 # the pytest_warning_captured hook was introduced in pytest 3.8
if hasattr(_pytest.hookspec, "pytest_warning_captured"): elif hasattr(_pytest.hookspec, "pytest_warning_captured"):
def pytest_warning_captured(self, warning_message, when, item): def pytest_warning_captured(self, warning_message, when, item):
self.sendevent( self.sendevent(
@@ -238,6 +252,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"])

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()
@@ -59,10 +63,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 +90,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 +110,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
@@ -154,11 +163,10 @@ 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(HostRSync, self).__init__(sourcedir=sourcedir, **kwargs)
def filter(self, path): def filter(self, path):
@@ -179,7 +187,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):
@@ -198,7 +206,7 @@ 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
@@ -222,6 +230,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
@@ -233,7 +242,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):
@@ -293,11 +302,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
@@ -319,7 +328,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":
@@ -358,8 +367,19 @@ 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

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(
""" """
@@ -328,7 +331,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,7 +593,11 @@ def test_fixture_teardown_failure(testdir):
assert result.ret assert result.ret
def test_config_initialization(testdir, pytestconfig): @pytest.mark.skipif(
sys.version_info[:2] == (2, 7),
reason="Only available in pytest 5.0+ (Python 3 only)",
)
def test_config_initialization(testdir, monkeypatch, pytestconfig):
"""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"): if not hasattr(pytestconfig, "invocation_params"):
pytest.skip( pytest.skip(
@@ -599,7 +606,8 @@ def test_config_initialization(testdir, pytestconfig):
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 +618,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"])
@@ -817,6 +827,32 @@ class TestWarnings:
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
@@ -1066,6 +1102,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(

View File

@@ -22,12 +22,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

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

@@ -57,6 +57,7 @@ def test_auto_detect_cpus(testdir, monkeypatch):
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")
@@ -95,6 +96,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,8 +1,8 @@
[tox] [tox]
envlist= envlist=
linting linting
py{27,34,35,36,37,38}-pytestlatest py{27,35,36,37,38}-pytestlatest
py38-pytest{master,features} py38-pytestmaster
[testenv] [testenv]
passenv = USER USERNAME passenv = USER USERNAME
@@ -10,7 +10,6 @@ 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}