Compare commits

..

79 Commits

Author SHA1 Message Date
Bruno Oliveira
bea2467712 Release 1.23.0 2018-08-23 16:33:06 -03:00
Bruno Oliveira
c4a793de38 Merge pull request #332 from nicoddemus/optimize-report-data
Optimize collection performance
2018-08-23 16:31:09 -03:00
Bruno Oliveira
9bcd0e4352 Optimize collection performance
Only send failed collection reports, as master is only interested in failed
reports.

Fix #330
2018-08-18 23:09:00 -03:00
Bruno Oliveira
96af694bf3 Merge pull request #321 from nicoddemus/release-1.22.5
Revert dropped support to old pytest versions
2018-07-28 09:50:50 -03:00
Bruno Oliveira
a72f25335b Merge remote-tracking branch 'upstream/master' into release-1.22.5 2018-07-28 09:12:22 -03:00
Bruno Oliveira
f92242652d Include six as dependency 2018-07-28 09:06:04 -03:00
Ronny Pfannschmidt
73e40aaa1b Merge pull request #322 from nicoddemus/black-pre-commit
Introduce pre-commit and black
2018-07-28 09:24:15 +02:00
Bruno Oliveira
9f9eee6b74 Remove flakes and readme from appveyor and travis 2018-07-27 18:42:06 -03:00
Bruno Oliveira
f333588b6a Skip sdist and usedevelop for linting 2018-07-27 18:18:03 -03:00
Bruno Oliveira
7ec8e4310c Fix line ending issues 2018-07-27 18:15:58 -03:00
Bruno Oliveira
9e14e78d07 Add show-diff-on-failures to linting call 2018-07-27 17:57:09 -03:00
Bruno Oliveira
da7ab1103c Add badge for black 2018-07-27 17:55:12 -03:00
Bruno Oliveira
fced1645cb Run pre-commit: black, whitespaces, rst 2018-07-27 17:50:55 -03:00
Bruno Oliveira
1ed3884da9 Add pre-commit files and linting using pre-commit 2018-07-27 17:46:25 -03:00
Bruno Oliveira
beb92732ed Include pytest 3.6 into the build matrix 2018-07-27 17:20:50 -03:00
Bruno Oliveira
dba68518fc Prepare release 1.22.5 2018-07-27 17:07:40 -03:00
Bruno Oliveira
b9d91e913e Add changelog entry for #321 2018-07-27 17:06:11 -03:00
Bruno Oliveira
b8154cf655 Revert "Drop support to old pytest versions (<3.4)"
This reverts commit b5abe791ad.
2018-07-27 17:02:20 -03:00
Bruno Oliveira
3776f3b74b Merge pull request #318 from nicoddemus/release-1.22.4
Release 1.22.4
2018-07-27 12:25:27 -03:00
Bruno Oliveira
c5f0c34d38 Prepare release 1.22.4 2018-07-27 09:17:18 -03:00
Bruno Oliveira
0b638ef02e Fix ponctuation in CHANGELOG 2018-07-27 09:16:53 -03:00
Bruno Oliveira
a9da3a5306 Update CHANGELOG template to put issue number in the start of the line
This makes using multiple paragraphs per entry possible
2018-07-27 09:15:04 -03:00
Bruno Oliveira
b814db45ce Merge pull request #317 from skirpichev/pypy3-numprocesses
Workaround cpu detection on Travis CI
2018-07-27 09:02:55 -03:00
Bruno Oliveira
ee0754bc35 Improve test for Travis fallback 2018-07-27 08:24:34 -03:00
Sergey B Kirpichev
5c59a30277 Add coverage test for Travis CI workaround 2018-07-26 12:56:10 +03:00
Sergey B Kirpichev
d3ef33ab32 Refactor automatic cpu detection code to auto_detect_cpus() 2018-07-26 12:52:18 +03:00
Sergey B Kirpichev
c950d88204 Workaround cpu detection on Travis CI
Closes #316
2018-07-25 19:05:19 +03:00
Bruno Oliveira
46119eb593 Merge pull request #315 from nicoddemus/pylib-woes
Remove last references to obsolete py.code
2018-07-25 09:07:19 -03:00
Bruno Oliveira
b5abe791ad Drop support to old pytest versions (<3.4)
Let's take the opportunity to reduce the build matrix and test with latest
pytest versions as well
2018-07-23 19:32:56 -03:00
Bruno Oliveira
fe1f239612 Remove last references to obsolete py.code
Also remove some unnecessary references to py.builtin

Fix #305
2018-07-23 17:36:03 -03:00
Bruno Oliveira
55dc257f1c Merge pull request #314 from nicoddemus/skip-tags-appveyor
Skip AppVeyor when building on tags
2018-07-23 17:14:14 -03:00
Bruno Oliveira
46dbc1223b Merge pull request #313 from nicoddemus/conda-forge-badge
Replace anaconda's badge by conda-forge's
2018-07-23 17:14:03 -03:00
Bruno Oliveira
9b46df48a6 Merge pull request #312 from nicoddemus/release-1.22.3
Release 1.22.3
2018-07-23 17:13:26 -03:00
Bruno Oliveira
c6306e4fbe Skip AppVeyor when building on tags 2018-07-23 17:03:06 -03:00
Bruno Oliveira
6ec967a3f2 Prepare release 1.22.3 2018-07-23 16:36:39 -03:00
Bruno Oliveira
0722ea12ef Use a tox environment to update the release files 2018-07-23 16:36:26 -03:00
Bruno Oliveira
a5d51f24c9 Replace anaconda's badge by conda-forge's 2018-07-23 16:33:05 -03:00
Ronny Pfannschmidt
bb91f5459d Merge pull request #309 from greut/cpu-count
plugin: get cpu_count from sched_getaffinity
2018-07-23 20:13:45 +02:00
Bruno Oliveira
00b7c4553a Merge pull request #311 from nicoddemus/skip-deploy
Skip deploy job on Travis if we are not on a tag
2018-07-21 19:20:25 -03:00
Bruno Oliveira
9ff6fc10d0 Skip deploy job on Travis if we are not on a tag 2018-07-21 18:17:21 -03:00
Bruno Oliveira
a06e75cc78 Fix flake8 issue 2018-07-21 12:09:44 -03:00
Yoan Blanc
04882074e1 plugin: get cpu_count from sched_getaffinity
Signed-off-by: Yoan Blanc <yoan.blanc@exoscale.ch>
2018-07-18 08:32:01 +02:00
Bruno Oliveira
06593498cd Merge pull request #306 from asottile/factor_out_py_std
Factor out py.std
2018-07-06 22:21:56 -03:00
Bruno Oliveira
57d09c2622 Merge pull request #308 from scop/dropthedot
Use pytest instead of py.test per upstream recommendation, #dropthedot
2018-07-06 22:20:53 -03:00
Ville Skyttä
663bbd5338 Use pytest instead of py.test per upstream recommendation, #dropthedot
http://blog.pytest.org/2016/whats-new-in-pytest-30/
https://twitter.com/hashtag/dropthedot
2018-07-07 00:01:11 +03:00
Bruno Oliveira
137b939a27 Merge pull request #307 from scop/grammar
Grammar fix
2018-07-06 12:05:26 -03:00
Ville Skyttä
17f6257a24 Grammar fix 2018-07-06 15:45:41 +03:00
Anthony Sottile
78fcf725cf Factor out py.std 2018-07-05 15:35:39 -07:00
Ronny Pfannschmidt
8d94fca964 Merge pull request #297 from nicoddemus/fix-init
Fix __init__ declaration
2018-06-11 06:01:17 +02:00
Bruno Oliveira
b06c52a5d2 Add .pytest_cache to gitignore 2018-06-03 16:10:55 -03:00
Bruno Oliveira
fd2e8a6711 Make all classes subclass from object 2018-06-03 16:10:24 -03:00
Bruno Oliveira
98ae25c12c Fix __init__ declaration 2018-06-03 16:06:52 -03:00
Bruno Oliveira
182d1f6433 Merge pull request #287 from nicoddemus/release-1.22.2
Preparing 1.22.2 release
2018-02-27 07:43:11 -03:00
Bruno Oliveira
c92bbb021a Preparing 1.22.2 release 2018-02-26 18:46:11 -03:00
Bruno Oliveira
710b324902 Merge pull request #286 from nicoddemus/fix-slaveoutput
Add backward compatibility for ``slaveoutput`` attribute to ``WorkerController`` instances
2018-02-26 18:44:24 -03:00
Bruno Oliveira
76e7b8d0c0 Add backward compatibility for `slaveoutput attribute to WorkerController` instances
Fix #285
Fix pytest-dev/pytest-cov#191
2018-02-26 14:52:55 -03:00
Bruno Oliveira
63b329c251 Merge pull request #281 from nicoddemus/release-1.22.1
Release 1.22.1
2018-02-19 19:10:07 -03:00
Bruno Oliveira
de993e1ee3 Prepare changelog for 1.22.1 2018-02-19 18:10:50 -03:00
Bruno Oliveira
af4409753a Show content for trivial changelog entries 2018-02-19 18:10:10 -03:00
Bruno Oliveira
c39cd10ae5 Small improvements to HOWTORELEASE 2018-02-19 18:07:13 -03:00
Ronny Pfannschmidt
11e3599785 Merge pull request #277 from lmtierney/single-loadscope
Ensure loadscope and loadfile work when the first scope has one work unit
2018-02-19 20:02:22 +01:00
Bruno Oliveira
1be8a463ff Add docstring to tests for #277 2018-02-16 20:14:47 -02:00
lmtierney
bd3c7dba8b Ensure loadscope and loadfile work when the first scope has one work unit 2018-02-08 19:51:42 -06:00
Ronny Pfannschmidt
d91ec5503e Merge pull request #262 from nicoddemus/titles-readme
Improve README to be more newcomer friendly
2018-02-08 07:49:08 +01:00
Bruno Oliveira
23ce4502f1 Merge pull request #268 from feuillemorte/234-master-worker-terminology
#234 use master/worker terminology
2018-02-06 07:27:31 -02:00
feuillemorte
6fb01801ac #234 Fix comments 2018-02-06 11:53:45 +03:00
Bruno Oliveira
ee2a1b4f4d Add an explicit test for backward compatibility with "config.slaveinput"
Related to #234
2018-01-30 19:13:14 -02:00
feuillemorte
b04703b6ba #234 Fix flake 8 and fix comments 2018-01-17 23:34:39 +03:00
feuillemorte
9a5a81962c Merge branch '234-master-worker-terminology' of github.com:feuillemorte/pytest-xdist into 234-master-worker-terminology 2018-01-17 23:07:45 +03:00
feuillemorte
40491279ef #234 Added master/worker terminology 2018-01-17 23:07:29 +03:00
Bruno Oliveira
9efe14946a Improve changelog formatting 2018-01-16 18:26:50 -02:00
feuillemorte
708256228b Add changelog entry 2018-01-16 21:49:54 +03:00
feuillemorte
f1e6dc4344 use master/worker terminology 2018-01-16 21:46:43 +03:00
Bruno Oliveira
3098f27731 Merge pull request #267 from nicoddemus/release-1.22.0
Prepare release 1.22.0
2018-01-11 18:13:41 -02:00
Bruno Oliveira
e72eadb413 Fix linting 2017-12-22 17:30:47 -02:00
Bruno Oliveira
6cd1eb2f43 Use more modern python and reword part about pytest ini configuration 2017-12-22 16:53:18 -02:00
Bruno Oliveira
7c3a86a549 Mention pytest-forked instead of the help for --boxed 2017-12-22 16:49:14 -02:00
Bruno Oliveira
b2c63a1ba5 Reworded some sections to be more newbie friendly 2017-12-22 16:42:35 -02:00
Bruno Oliveira
3f545ee621 Change section titles so they are more prominent in the README 2017-12-22 16:29:10 -02:00
38 changed files with 1677 additions and 1303 deletions

View File

@@ -13,5 +13,3 @@ Here's a quick checklist that should be present in PRs:
``` ```
Fix issue with non-ascii contents in doctest text files. Fix issue with non-ascii contents in doctest text files.
``` ```

2
.gitignore vendored
View File

@@ -22,6 +22,7 @@ dist/
include/ include/
lib/ lib/
bin/ bin/
env/
xdist/_version.py* xdist/_version.py*
pytest_xdist.egg-info pytest_xdist.egg-info
issue/ issue/
@@ -29,5 +30,6 @@ issue/
pytestdebug.log pytestdebug.log
.tox/ .tox/
.cache/ .cache/
.pytest_cache/
.eggs/ .eggs/
.idea/ .idea/

24
.pre-commit-config.yaml Normal file
View File

@@ -0,0 +1,24 @@
repos:
- repo: https://github.com/ambv/black
rev: 18.6b4
hooks:
- id: black
args: [--safe, --quiet]
language_version: python3.6
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v1.3.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: debug-statements
- id: flake8
- repo: local
hooks:
- id: rst
name: rst
entry: rst-lint --encoding utf-8
files: ^(CHANGELOG.rst|HOWTORELEASE.rst|README.rst|changelog/.*)$
language: python
additional_dependencies: [pygments, restructuredtext_lint]
python_version: python3.6

View File

@@ -21,12 +21,23 @@ env:
- TOXENV=py-pytest31 - TOXENV=py-pytest31
- TOXENV=py-pytest32 - TOXENV=py-pytest32
- TOXENV=py-pytest33 - TOXENV=py-pytest33
- TOXENV=py-pytest36
install: pip install tox setuptools_scm install: pip install tox setuptools_scm
script: tox script: tox
stages:
- linting
- test
- name: deploy
if: repo = pytest-dev/pytest-xdist AND tag IS present
jobs: jobs:
include: include:
- stage: linting
python: '3.6'
script:
- tox -e linting
- stage: test - stage: test
# python x env above are already included into this stage # python x env above are already included into this stage
- python: "2.7" - python: "2.7"
@@ -37,10 +48,6 @@ jobs:
env: TOXENV=py36-pytestmaster env: TOXENV=py36-pytestmaster
- python: "3.6" - python: "3.6"
env: TOXENV=py36-pytestfeatures env: TOXENV=py36-pytestfeatures
- python: "3.6"
env: TOXENV=flakes
- python: "3.6"
env: TOXENV=readme
- stage: deploy - stage: deploy
python: '3.6' python: '3.6'
@@ -50,7 +57,7 @@ jobs:
deploy: deploy:
provider: pypi provider: pypi
user: ronny user: ronny
distributions: sdist bdist_wheel distributions: sdist bdist_wheel
skip_upload_docs: true skip_upload_docs: true
password: password:
secure: cxmSDho5d+PYKEM4ZCg8ms1P4lzhYkrw6fEOm2HtTcsuCyY6aZMSgImWAnEYbJHSkdzgcxlXK9UKJ9B0YenXmBCkAr7UjdnpNXNmkySr0sYzlH/sfqt/dDATCHFaRKxnkOSOVywaDYhT9n8YudbXI77pXwD12i/CeSSJDbHhsu0JYUfAcb+D6YjRYoA2SEGCnzSzg+gDDfwXZx4ZiODCGLVwieNp1klCg88YROUE1BaYYNuUOONvfXX8+TWowbCF6ChH1WL/bZ49OStEYQNuYxZQZr4yClIqu9VJbchrU8j860K9ott2kkGTgfB/dDrQB/XncBubyIX9ikzCQAmmBXWAI3eyvWLPDk2Jz7kW2l2RT7syct80tCq3JhvQ1qdwr5ap7siocTLgnBW0tF4tkHSTFN3510fkc43npnp6FThebESQpnI24vqpwJ9hI/kW5mYi014Og2E/cpCXnz2XO8iZPDbqAMQpDsqEQoyhfGNgPTGp4K30TxRtwZBI5hHhDKnnR16fXtRgt1gYPvz/peUQvvpOm4JzIzGXPzluuutpnCBy75v5+oiwT3YRrLL/Meims9FtDDXL3qQubAE/ezIOOpm0N5XXV8DxIom8EN71yq5ab1tqhM+tBX7owRjy4FR4If2Q8feBdmTuh26DIQt/y+qSG8VkB9Sw/JCjc7c= secure: cxmSDho5d+PYKEM4ZCg8ms1P4lzhYkrw6fEOm2HtTcsuCyY6aZMSgImWAnEYbJHSkdzgcxlXK9UKJ9B0YenXmBCkAr7UjdnpNXNmkySr0sYzlH/sfqt/dDATCHFaRKxnkOSOVywaDYhT9n8YudbXI77pXwD12i/CeSSJDbHhsu0JYUfAcb+D6YjRYoA2SEGCnzSzg+gDDfwXZx4ZiODCGLVwieNp1klCg88YROUE1BaYYNuUOONvfXX8+TWowbCF6ChH1WL/bZ49OStEYQNuYxZQZr4yClIqu9VJbchrU8j860K9ott2kkGTgfB/dDrQB/XncBubyIX9ikzCQAmmBXWAI3eyvWLPDk2Jz7kW2l2RT7syct80tCq3JhvQ1qdwr5ap7siocTLgnBW0tF4tkHSTFN3510fkc43npnp6FThebESQpnI24vqpwJ9hI/kW5mYi014Og2E/cpCXnz2XO8iZPDbqAMQpDsqEQoyhfGNgPTGp4K30TxRtwZBI5hHhDKnnR16fXtRgt1gYPvz/peUQvvpOm4JzIzGXPzluuutpnCBy75v5+oiwT3YRrLL/Meims9FtDDXL3qQubAE/ezIOOpm0N5XXV8DxIom8EN71yq5ab1tqhM+tBX7owRjy4FR4If2Q8feBdmTuh26DIQt/y+qSG8VkB9Sw/JCjc7c=

View File

@@ -1,3 +1,83 @@
pytest-xdist 1.23.0 (2018-08-23)
================================
Features
--------
- `#330 <https://github.com/pytest-dev/pytest-xdist/issues/330>`_: Improve collection performance by reducing the number of events sent to ``master`` node.
pytest-xdist 1.22.5 (2018-07-27)
================================
Bug Fixes
---------
- `#321 <https://github.com/pytest-dev/pytest-xdist/issues/321>`_: Revert change that dropped support for ``pytest<3.4`` and require ``six``.
This change caused problems in some installations, and was a mistaken
in the first place as we should not change version requirements
in bug-fix releases unless they fix an actual bug.
pytest-xdist 1.22.4 (2018-07-27)
================================
Bug Fixes
---------
- `#305 <https://github.com/pytest-dev/pytest-xdist/issues/305>`_: Remove last references to obsolete ``py.code``.
Remove some unnecessary references to ``py.builtin``.
- `#316 <https://github.com/pytest-dev/pytest-xdist/issues/316>`_: Workaround cpu detection on Travis CI.
pytest-xdist 1.22.3 (2018-07-23)
================================
Bug Fixes
---------
- Fix issue of virtualized or containerized environments not reporting the number of CPUs correctly. (`#9 <https://github.com/pytest-dev/pytest-xdist/issues/9>`_)
Trivial Changes
---------------
- Make all classes subclass from ``object`` and fix ``super()`` call in ``LoadFileScheduling``; (`#297 <https://github.com/pytest-dev/pytest-xdist/issues/297>`_)
pytest-xdist 1.22.2 (2018-02-26)
================================
Bug Fixes
---------
- Add backward compatibility for ``slaveoutput`` attribute to
``WorkerController`` instances. (`#285
<https://github.com/pytest-dev/pytest-xdist/issues/285>`_)
pytest-xdist 1.22.1 (2018-02-19)
================================
Bug Fixes
---------
- Fix issue when using ``loadscope`` or ``loadfile`` where tests would fail to
start if the first scope had only one test. (`#257
<https://github.com/pytest-dev/pytest-xdist/issues/257>`_)
Trivial Changes
---------------
- Change terminology used by ``pytest-xdist`` to *master* and *worker* in
arguments and messages (for example ``--max-worker-reset``). (`#234
<https://github.com/pytest-dev/pytest-xdist/issues/234>`_)
pytest-xdist 1.22.0 (2018-01-11) pytest-xdist 1.22.0 (2018-01-11)
================================ ================================

View File

@@ -24,15 +24,13 @@ To publish a new release ``X.Y.Z``, the steps are as follows:
#. Create a new branch named ``release-X.Y.Z`` from the latest ``master``. #. Create a new branch named ``release-X.Y.Z`` from the latest ``master``.
#. Install ``pytest-xdist`` and dev requirements in a virtualenv:: #. Install ``tox`` in a virtualenv::
$ pip install -e . -r dev-requirements.txt $ pip install tox
#. Update ``CHANGELOG.rst`` file by running:: #. Update the necessary files with::
$ towncrier --version X.Y.Z $ tox -e release -- X.Y.Z
It might ask for confirmation to remove news fragments; answer yes.
#. Commit and push the branch for review. #. Commit and push the branch for review.

View File

@@ -19,7 +19,7 @@ tag: feature
allow to run xdist own tests using its own mechanism. allow to run xdist own tests using its own mechanism.
currently this doesn't work because the remote side currently this doesn't work because the remote side
has no py.test plugin. How to configure/do has no pytest plugin. How to configure/do
register "xdist.plugin" on the remote side? register "xdist.plugin" on the remote side?
see to avoid any "from _pytest" internal imports see to avoid any "from _pytest" internal imports

View File

@@ -5,10 +5,10 @@
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions: furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software. copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
@@ -16,4 +16,3 @@
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE. SOFTWARE.

View File

@@ -1,78 +1,76 @@
# Overview # # Overview #
`xdist` works by spawning one or more **workers**, which are controlled `xdist` works by spawning one or more **workers**, which are controlled
by the **master**. Each **worker** is responsible for performing by the **master**. Each **worker** is responsible for performing
a full test collection and afterwards running tests as dictated by the **master**. a full test collection and afterwards running tests as dictated by the **master**.
The execution flow is: The execution flow is:
1. **master** spawns one or more **workers** at the beginning of 1. **master** spawns one or more **workers** at the beginning of
the test session. The communication between **master** and **worker** nodes makes use of the test session. The communication between **master** and **worker** nodes makes use of
[execnet](http://codespeak.net/execnet/) and its [gateways](http://codespeak.net/execnet/basics.html#gateways-bootstrapping-python-interpreters). [execnet](http://codespeak.net/execnet/) and its [gateways](http://codespeak.net/execnet/basics.html#gateways-bootstrapping-python-interpreters).
The actual interpreters executing the code for the **workers** might The actual interpreters executing the code for the **workers** might
be remote or local. be remote or local.
1. Each **worker** itself is a mini pytest runner. **workers** at this 1. Each **worker** itself is a mini pytest runner. **workers** at this
point perform a full test collection, sending back the collected point perform a full test collection, sending back the collected
test-ids back to the **master** which does not test-ids back to the **master** which does not
perform any collection itself. perform any collection itself.
1. The **master** receives the result of the collection from all nodes. 1. The **master** receives the result of the collection from all nodes.
At this point the **master** performs some sanity check to ensure that At this point the **master** performs some sanity check to ensure that
all **workers** collected the same tests (including order), bailing out otherwise. all **workers** collected the same tests (including order), bailing out otherwise.
If all is well, it converts the list of test-ids into a list of simple If all is well, it converts the list of test-ids into a list of simple
indexes, where each index corresponds to the position of that test in the indexes, where each index corresponds to the position of that test in the
original collection list. This works because all nodes have the same original collection list. This works because all nodes have the same
collection list, and saves bandwidth because the **master** can now tell collection list, and saves bandwidth because the **master** can now tell
one of the workers to just *execute test index 3* index of passing the one of the workers to just *execute test index 3* index of passing the
full test id. full test id.
1. If **dist-mode** is **each**: the **master** just sends the full list 1. If **dist-mode** is **each**: the **master** just sends the full list
of test indexes to each node at this moment. of test indexes to each node at this moment.
1. If **dist-mode** is **load**: the **master** takes around 25% of the 1. If **dist-mode** is **load**: the **master** takes around 25% of the
tests and sends them one by one to each **worker** in a round robin tests and sends them one by one to each **worker** in a round robin
fashion. The rest of the tests will be distributed later as **workers** fashion. The rest of the tests will be distributed later as **workers**
finish tests (see below). finish tests (see below).
1. Note that `pytest_xdist_make_scheduler` hook can be used to implement custom tests distribution logic. 1. Note that `pytest_xdist_make_scheduler` hook can be used to implement custom tests distribution logic.
1. **workers** re-implement `pytest_runtestloop`: pytest's default implementation 1. **workers** re-implement `pytest_runtestloop`: pytest's default implementation
basically loops over all collected items in the `session` object and executes basically loops over all collected items in the `session` object and executes
the `pytest_runtest_protocol` for each test item, but in xdist **workers** sit idly the `pytest_runtest_protocol` for each test item, but in xdist **workers** sit idly
waiting for **master** to send tests for execution. As tests are waiting for **master** to send tests for execution. As tests are
received by **workers**, `pytest_runtest_protocol` is executed for each test. received by **workers**, `pytest_runtest_protocol` is executed for each test.
Here it worth noting an implementation detail: **workers** always must keep at Here it worth noting an implementation detail: **workers** always must keep at
least one test item on their queue due to how the `pytest_runtest_protocol(item, nextitem)` least one test item on their queue due to how the `pytest_runtest_protocol(item, nextitem)`
hook is defined: in order to pass the `nextitem` to the hook, the worker must wait for more hook is defined: in order to pass the `nextitem` to the hook, the worker must wait for more
instructions from master before executing that remaining test. If it receives more tests, instructions from master before executing that remaining test. If it receives more tests,
then it can safely call `pytest_runtest_protocol` because it knows what the `nextitem` parameter will be. then it can safely call `pytest_runtest_protocol` because it knows what the `nextitem` parameter will be.
If it receives a "shutdown" signal, then it can execute the hook passing `nextitem` as `None`. If it receives a "shutdown" signal, then it can execute the hook passing `nextitem` as `None`.
1. As tests are started and completed at the **workers**, the results are sent 1. As tests are started and completed at the **workers**, the results are sent
back to the **master**, which then just forwards the results to back to the **master**, which then just forwards the results to
the appropriate pytest hooks: `pytest_runtest_logstart` and the appropriate pytest hooks: `pytest_runtest_logstart` and
`pytest_runtest_logreport`. This way other plugins (for example `junitxml`) `pytest_runtest_logreport`. This way other plugins (for example `junitxml`)
can work normally. The **master** (when in dist-mode **load**) can work normally. The **master** (when in dist-mode **load**)
decides to send more tests to a node when a test completes, using decides to send more tests to a node when a test completes, using
some heuristics such as test durations and how many tests each **worker** some heuristics such as test durations and how many tests each **worker**
still has to run. still has to run.
1. When the **master** has no more pending tests it will 1. When the **master** has no more pending tests it will
send a "shutdown" signal to all **workers**, which will then run their send a "shutdown" signal to all **workers**, which will then run their
remaining tests to completion and shut down. At this point the remaining tests to completion and shut down. At this point the
**master** will sit waiting for **workers** to shut down, still **master** will sit waiting for **workers** to shut down, still
processing events such as `pytest_runtest_logreport`. processing events such as `pytest_runtest_logreport`.
## FAQ ## ## FAQ ##
> Why does each worker do its own collection, as opposed to having > Why does each worker do its own collection, as opposed to having
the master collect once and distribute from that collection to the workers? the master collect once and distribute from that collection to the workers?
If collection was performed by master then it would have to If collection was performed by master then it would have to
serialize collected items to send them through the wire, as workers live in another process. serialize collected items to send them through the wire, as workers live in another process.
The problem is that test items are not easily (impossible?) to serialize, as they contain references to The problem is that test items are not easily (impossible?) to serialize, as they contain references to
the test functions, fixture managers, config objects, etc. Even if one manages to serialize it, the test functions, fixture managers, config objects, etc. Even if one manages to serialize it,
it seems it would be very hard to get it right and easy to break by any small change in pytest. it seems it would be very hard to get it right and easy to break by any small change in pytest.

View File

@@ -4,14 +4,13 @@
:alt: PyPI version :alt: PyPI version
:target: https://pypi.python.org/pypi/pytest-xdist :target: https://pypi.python.org/pypi/pytest-xdist
.. image:: https://img.shields.io/conda/vn/conda-forge/pytest-xdist.svg
:target: https://anaconda.org/conda-forge/pytest-xdist
.. image:: https://img.shields.io/pypi/pyversions/pytest-xdist.svg .. image:: https://img.shields.io/pypi/pyversions/pytest-xdist.svg
:alt: Python versions :alt: Python versions
:target: https://pypi.python.org/pypi/pytest-xdist :target: https://pypi.python.org/pypi/pytest-xdist
.. image:: https://anaconda.org/conda-forge/pytest-xdist/badges/version.svg
:alt: Anaconda version
:target: https://anaconda.org/conda-forge/pytest-xdist
.. image:: https://travis-ci.org/pytest-dev/pytest-xdist.svg?branch=master .. image:: https://travis-ci.org/pytest-dev/pytest-xdist.svg?branch=master
:alt: Travis CI build status :alt: Travis CI build status
:target: https://travis-ci.org/pytest-dev/pytest-xdist :target: https://travis-ci.org/pytest-dev/pytest-xdist
@@ -20,10 +19,13 @@
:alt: AppVeyor build status :alt: AppVeyor build status
:target: https://ci.appveyor.com/project/pytestbot/pytest-xdist :target: https://ci.appveyor.com/project/pytestbot/pytest-xdist
xdist: pytest distributed testing plugin .. image:: https://img.shields.io/badge/code%20style-black-000000.svg
========================================= :target: https://github.com/ambv/black
The `pytest-xdist`_ plugin extends py.test with some unique xdist: pytest distributed testing plugin
========================================
The `pytest-xdist`_ plugin extends pytest with some unique
test execution modes: test execution modes:
* test run parallelization_: if you have multiple CPUs or hosts you can use * test run parallelization_: if you have multiple CPUs or hosts you can use
@@ -32,24 +34,24 @@ test execution modes:
* ``--looponfail``: run your tests repeatedly in a subprocess. After each run * ``--looponfail``: run your tests repeatedly in a subprocess. After each run
py.test waits until a file in your project changes and then re-runs pytest waits until a file in your project changes and then re-runs
the previously failing tests. This is repeated until all tests pass the previously failing tests. This is repeated until all tests pass
after which again a full run is performed. after which again a full run is performed.
* `Multi-Platform`_ coverage: you can specify different Python interpreters * `Multi-Platform`_ coverage: you can specify different Python interpreters
or different platforms and run tests in parallel on all of them. or different platforms and run tests in parallel on all of them.
Before running tests remotely, ``py.test`` efficiently "rsyncs" your Before running tests remotely, ``pytest`` efficiently "rsyncs" your
program source code to the remote place. All test results program source code to the remote place. All test results
are reported back and displayed to your local terminal. are reported back and displayed to your local terminal.
You may specify different Python versions and interpreters. You may specify different Python versions and interpreters.
If you would like to know how pytest-xdist works under the covers, checkout If you would like to know how pytest-xdist works under the covers, checkout
`OVERVIEW <https://github.com/pytest-dev/pytest-xdist/blob/master/OVERVIEW.md>`_. `OVERVIEW <https://github.com/pytest-dev/pytest-xdist/blob/master/OVERVIEW.md>`_.
Installation Installation
----------------------- ------------
Install the plugin with:: Install the plugin with::
@@ -58,43 +60,54 @@ Install the plugin with::
or use the package in develop/in-place mode with or use the package in develop/in-place mode with
a checkout of the `pytest-xdist repository`_ :: a checkout of the `pytest-xdist repository`_ ::
python setup.py develop pip install --editable .
Usage examples
---------------------
.. _parallelization: .. _parallelization:
Speed up test runs by sending tests to multiple CPUs Speed up test runs by sending tests to multiple CPUs
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ ----------------------------------------------------
To send tests to multiple CPUs, type:: To send tests to multiple CPUs, type::
py.test -n NUM pytest -n NUM
Especially for longer running tests or tests requiring Especially for longer running tests or tests requiring
a lot of IO this can lead to considerable speed ups. This option can a lot of I/O this can lead to considerable speed ups. This option can
also be set to ``auto`` for automatic detection of the number of CPUs. also be set to ``auto`` for automatic detection of the number of CPUs.
If a test crashes the interpreter, pytest-xdist will automatically restart If a test crashes the interpreter, pytest-xdist will automatically restart
that slave and report the failure as usual. You can use the that worker and report the failure as usual. You can use the
``--max-slave-restart`` option to limit the number of slaves that can ``--max-worker-restart`` option to limit the number of workers that can
be restarted, or disable restarting altogether using ``--max-slave-restart=0``. be restarted, or disable restarting altogether using ``--max-worker-restart=0``.
By default, the ``-n`` option will send pending tests to any worker that is available, without
any guaranteed order, but you can control this with these options:
* ``--dist=loadscope``: tests will be grouped by **module** for *test functions* and
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
worker, guaranteeing that all tests in a group run in the same worker. This feature was added
in version ``1.21``.
Running tests in a Python subprocess Running tests in a Python subprocess
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ ------------------------------------
To instantiate a python2.5 sub process and send tests to it, you may type:: To instantiate a python3.5 subprocess and send tests to it, you may type::
py.test -d --tx popen//python=python2.5 pytest -d --tx popen//python=python3.5
This will start a subprocess which is run with the "python2.5" This will start a subprocess which is run with the ``python3.5``
Python interpreter, found in your system binary lookup path. Python interpreter, found in your system binary lookup path.
If you prefix the --tx option value like this:: If you prefix the --tx option value like this::
--tx 3*popen//python=python2.5 --tx 3*popen//python=python3.5
then three subprocesses would be created and tests then three subprocesses would be created and tests
will be load-balanced across these three processes. will be load-balanced across these three processes.
@@ -102,38 +115,26 @@ will be load-balanced across these three processes.
.. _boxed: .. _boxed:
Running tests in a boxed subprocess Running tests in a boxed subprocess
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -----------------------------------
If you have tests involving C or C++ libraries you might have to deal
with tests crashing the process. For this case you may use the boxing
options::
py.test --boxed
which will run each test in a subprocess and will report if a test
crashed the process. You can also combine this option with
running multiple processes to speed up the test run and use your CPU cores::
py.test -n3 --boxed
this would run 3 testing subprocesses in parallel which each
create new boxed subprocesses for each test.
This functionality has been moved to the
`pytest-forked <https://github.com/pytest-dev/pytest-forked>`_ plugin, but the ``--boxed`` option
is still kept for backward compatibility.
.. _`remote machines`: .. _`remote machines`:
Sending tests to remote SSH accounts Sending tests to remote SSH accounts
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ ------------------------------------
Suppose you have a package ``mypkg`` which contains some Suppose you have a package ``mypkg`` which contains some
tests that you can successfully run locally. And you tests that you can successfully run locally. And you
have a ssh-reachable machine ``myhost``. Then have a ssh-reachable machine ``myhost``. Then
you can ad-hoc distribute your tests by typing:: you can ad-hoc distribute your tests by typing::
py.test -d --tx ssh=myhostpopen --rsyncdir mypkg mypkg pytest -d --tx ssh=myhostpopen --rsyncdir mypkg mypkg
This will synchronize your :code:`mypkg` package directory This will synchronize your :code:`mypkg` package directory
to an remote ssh account and then locally collect tests to a remote ssh account and then locally collect tests
and send them to remote places for execution. and send them to remote places for execution.
You can specify multiple :code:`--rsyncdir` directories You can specify multiple :code:`--rsyncdir` directories
@@ -141,11 +142,11 @@ to be sent to the remote side.
.. note:: .. note::
For py.test to collect and send tests correctly For pytest to collect and send tests correctly
you not only need to make sure all code and tests you not only need to make sure all code and tests
directories are rsynced, but that any test (sub) directory directories are rsynced, but that any test (sub) directory
also has an :code:`__init__.py` file because internally also has an :code:`__init__.py` file because internally
py.test references tests as a fully qualified python pytest references tests as a fully qualified python
module path. **You will otherwise get strange errors** module path. **You will otherwise get strange errors**
during setup of the remote side. during setup of the remote side.
@@ -158,7 +159,7 @@ ini-file option(s).
Sending tests to remote Socket Servers Sending tests to remote Socket Servers
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ --------------------------------------
Download the single-module `socketserver.py`_ Python program Download the single-module `socketserver.py`_ Python program
and run it like this:: and run it like this::
@@ -169,7 +170,7 @@ It will tell you that it starts listening on the default
port. You can now on your home machine specify this port. You can now on your home machine specify this
new socket host with something like this:: new socket host with something like this::
py.test -d --tx socket=192.168.1.102:8888 --rsyncdir mypkg mypkg pytest -d --tx socket=192.168.1.102:8888 --rsyncdir mypkg mypkg
.. _`atonce`: .. _`atonce`:
@@ -177,16 +178,16 @@ new socket host with something like this::
Running tests on many platforms at once Running tests on many platforms at once
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ ---------------------------------------
The basic command to run tests on multiple platforms is:: The basic command to run tests on multiple platforms is::
py.test --dist=each --tx=spec1 --tx=spec2 pytest --dist=each --tx=spec1 --tx=spec2
If you specify a windows host, an OSX host and a Linux If you specify a windows host, an OSX host and a Linux
environment this command will send each tests to all environment this command will send each tests to all
platforms - and report back failures from all platforms platforms - and report back failures from all platforms
at once. The specifications strings use the `xspec syntax`_. at once. The specifications strings use the `xspec syntax`_.
.. _`xspec syntax`: http://codespeak.net/execnet/basics.html#xspec .. _`xspec syntax`: http://codespeak.net/execnet/basics.html#xspec
@@ -195,7 +196,7 @@ at once. The specifications strings use the `xspec syntax`_.
.. _`execnet`: http://codespeak.net/execnet .. _`execnet`: http://codespeak.net/execnet
Identifying the worker process during a test Identifying the worker process during a test
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ --------------------------------------------
*New in version 1.15.* *New in version 1.15.*
@@ -219,16 +220,15 @@ defined:
* ``PYTEST_XDIST_WORKER_COUNT``: the total number of workers in this session, * ``PYTEST_XDIST_WORKER_COUNT``: the total number of workers in this session,
e.g., ``"4"`` when ``-n 4`` is given in the command-line. e.g., ``"4"`` when ``-n 4`` is given in the command-line.
The information about the worker_id in a test is stored in the TestReport as The information about the worker_id in a test is stored in the ``TestReport`` as
well, under worker_id attribute. well, under the ``worker_id`` attribute.
Specifying test exec environments in an ini file Specifying test exec environments in an ini file
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ ------------------------------------------------
pytest (since version 2.0) supports ini-style cofiguration. You can use pytest's ini file configuration to avoid typing common options.
You can for example make running with three subprocesses You can for example make running with three subprocesses your default like this:
your default like this:
.. code-block:: ini .. code-block:: ini
@@ -240,109 +240,17 @@ You can also add default environments like this:
.. code-block:: ini .. code-block:: ini
[pytest] [pytest]
addopts = --tx ssh=myhost//python=python2.5 --tx ssh=myhost//python=python3.6 addopts = --tx ssh=myhost//python=python3.5 --tx ssh=myhost//python=python3.6
and then just type:: and then just type::
py.test --dist=each pytest --dist=each
to run tests in each of the environments. to run tests in each of the environments.
Sending groups of related tests to the same worker
++++++++++++++++++++++++++++++++++++++++++++++++++
*New in version 1.19.*
.. note::
This is an **experimental** feature: the actual functionality will
likely stay the same, but the CLI might change slightly in future versions.
You can send groups of related tests to the same worker by using the
``--dist=loadscope`` option. Tests will be grouped by **module**
for *test functions* and by **class** for *test methods*.
For example, consider this two test files:
.. code-block:: python
# content of test_container.py
import pytest
def test_container_startup():
pass
def test_container_logging():
pass
@pytest.mark.parametrize('methods', ['ssh', 'http'])
def test_container_communication(methods):
pass
# content of test_io.py
class TestHDF:
def test_listing(self):
pass
def test_search(self):
pass
class TestXML:
def test_listing(self):
pass
def test_search(self):
pass
By executing ``pytest -v --dist=loadscope -n4`` you might get this output
(sorted by worker for readability)::
============================= test session starts =============================
<skip header>
gw0 [8] / gw1 [8] / gw2 [8] / gw3 [8]
scheduling tests via LoadScopeScheduling
[gw0] PASSED test_container.py::test_container_communication[http]
[gw0] PASSED test_container.py::test_container_communication[ssh]
[gw0] PASSED test_container.py::test_container_logging
[gw0] PASSED test_container.py::test_container_startup
[gw1] PASSED test_io.py::TestHDF::test_listing
[gw1] PASSED test_io.py::TestHDF::test_search
[gw2] PASSED test_io.py::TestXML::test_listing
[gw2] PASSED test_io.py::TestXML::test_search
========================== 8 passed in 0.56 seconds ===========================
As you can see, all test functions from ``test_container.py`` executed on
the same worker ``gw0``, while the test methods from classes ``TestHDF`` and
``TestXML`` executed in workers ``gw1`` and ``gw2`` respectively.
Currently the groupings can't be customized, with grouping by class takes
priority over grouping by module.
Sending tests to the same worker based on their file
++++++++++++++++++++++++++++++++++++++++++++++++++++
*New in version 1.21.*
.. note::
This is an **experimental** feature: the actual functionality will
likely stay the same, but the CLI might change slightly in future versions.
You can send tests to the same worker grouped by their filename by using the
``--dist=loadfile`` option, so tests of the same file are guaranteed to run
in the same worker.
Using the example in the previous section, all tests from ``test_container.py`` will
run in the same worker, as well as the tests in ``test_io.py``.
Specifying "rsync" dirs in an ini-file Specifying "rsync" dirs in an ini-file
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ --------------------------------------
In a ``tox.ini`` or ``setup.cfg`` file in your root project directory In a ``tox.ini`` or ``setup.cfg`` file in your root project directory
you may specify directories to include or to exclude in synchronisation: you may specify directories to include or to exclude in synchronisation:

View File

@@ -5,15 +5,18 @@ environment:
- TOXENV: "py34-pytest33" - TOXENV: "py34-pytest33"
- TOXENV: "py35-pytest33" - TOXENV: "py35-pytest33"
- TOXENV: "py36-pytest33" - TOXENV: "py36-pytest33"
- TOXENV: "py36-pytest36"
- TOXENV: "py27-pytest33-pexpect" - TOXENV: "py27-pytest33-pexpect"
- TOXENV: "py36-pytest33-pexpect" - TOXENV: "py36-pytest33-pexpect"
- TOXENV: "flakes"
- TOXENV: "readme"
install: install:
- C:\Python35\python -m pip install -U tox setuptools_scm pip - C:\Python36\python -m pip install -U tox setuptools_scm pip
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:\Python35\python -m tox - C:\Python36\python -m tox
# We don't deploy anything on tags with AppVeyor, we use Travis instead, so we
# might as well save resources
skip_tags: true

View File

@@ -13,8 +13,7 @@
{% if definitions[category]['showcontent'] %} {% if definitions[category]['showcontent'] %}
{% for text, values in sections[section][category]|dictsort(by='value') %} {% for text, values in sections[section][category]|dictsort(by='value') %}
- {{ text }}{% if category != 'vendor' %} (`{{ values[0] }} <https://github.com/pytest-dev/pytest-xdist/issues/{{ values[0][1:] }}>`_){% endif %} - `{{ values[0] }} <https://github.com/pytest-dev/pytest-xdist/issues/{{ values[0][1:] }}>`_: {{ text }}
{% endfor %} {% endfor %}
{% else %} {% else %}

View File

@@ -1 +0,0 @@
towncrier

View File

@@ -1,8 +1,8 @@
.. note:: .. note::
Since 1.19.0, the actual implementation of the ``--boxed`` option has been moved to a Since 1.19.0, the actual implementation of the ``--boxed`` option has been moved to a
separate plugin, `pytest-forked <https://github.com/pytest-dev/pytest-forked>`_ separate plugin, `pytest-forked <https://github.com/pytest-dev/pytest-forked>`_
which can be installed independently. The ``--boxed`` command-line options remains which can be installed independently. The ``--boxed`` command-line options remains
for backward compatibility reasons. for backward compatibility reasons.
@@ -25,7 +25,7 @@ to run each test in a controlled subprocess. Here is a basic example::
If you run this with:: If you run this with::
$ py.test -n1 $ pytest -n1
=========================== test session starts ============================ =========================== test session starts ============================
platform linux2 -- Python 2.7.3 -- pytest-2.3.0.dev8 platform linux2 -- Python 2.7.3 -- pytest-2.3.0.dev8
plugins: xdist, bugzilla, cache, oejskit, cli, pep8, cov plugins: xdist, bugzilla, cache, oejskit, cli, pep8, cov
@@ -46,7 +46,7 @@ You'll see that a couple of tests are reported as crashing, indicated
by lower-case ``f`` and the respective failure summary. You can also use by lower-case ``f`` and the respective failure summary. You can also use
the xdist-provided parallelization feature to speed up your testing:: the xdist-provided parallelization feature to speed up your testing::
$ py.test -n3 $ pytest -n3
=========================== test session starts ============================ =========================== test session starts ============================
platform linux2 -- Python 2.7.3 -- pytest-2.3.0.dev8 platform linux2 -- Python 2.7.3 -- pytest-2.3.0.dev8
plugins: xdist, bugzilla, cache, oejskit, cli, pep8, cov plugins: xdist, bugzilla, cache, oejskit, cli, pep8, cov

View File

@@ -3,7 +3,6 @@ from unittest import TestCase
class Delta1(TestCase): class Delta1(TestCase):
def test_delta0(self): def test_delta0(self):
sleep(5) sleep(5)
assert True assert True
@@ -46,7 +45,6 @@ class Delta1(TestCase):
class Delta2(TestCase): class Delta2(TestCase):
def test_delta0(self): def test_delta0(self):
sleep(5) sleep(5)
assert True assert True

View File

@@ -8,7 +8,7 @@ passenv = http_proxy https_proxy
deps = -rrequirements.txt deps = -rrequirements.txt
changedir = {envtmpdir} changedir = {envtmpdir}
commands = commands =
py.test -s -v \ pytest -s -v \
--doctest-modules \ --doctest-modules \
--junitxml=tests.xml \ --junitxml=tests.xml \
--dist=loadscope \ --dist=loadscope \

View File

@@ -33,4 +33,4 @@ template = "changelog/_template.rst"
[[tool.towncrier.type]] [[tool.towncrier.type]]
directory = "trivial" directory = "trivial"
name = "Trivial Changes" name = "Trivial Changes"
showcontent = false showcontent = true

View File

@@ -1,47 +1,44 @@
from setuptools import setup, find_packages from setuptools import setup, find_packages
install_requires = ['execnet>=1.1', 'pytest>=3.0.0', 'pytest-forked'] install_requires = ["execnet>=1.1", "pytest>=3.0.0", "pytest-forked", "six"]
setup( setup(
name="pytest-xdist", name="pytest-xdist",
use_scm_version={'write_to': 'xdist/_version.py'}, use_scm_version={"write_to": "xdist/_version.py"},
description='py.test xdist plugin for distributed testing' description="pytest xdist plugin for distributed testing"
' and loop-on-failing modes', " and loop-on-failing modes",
long_description=open('README.rst').read(), long_description=open("README.rst").read(),
license='MIT', license="MIT",
author='holger krekel and contributors', author="holger krekel and contributors",
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(exclude=["testing", "example"]),
entry_points={ entry_points={
'pytest11': [ "pytest11": ["xdist = xdist.plugin", "xdist.looponfail = xdist.looponfail"]
'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.*",
install_requires=install_requires, install_requires=install_requires,
setup_requires=['setuptools_scm'], setup_requires=["setuptools_scm"],
classifiers=[ classifiers=[
'Development Status :: 5 - Production/Stable', "Development Status :: 5 - Production/Stable",
'Framework :: Pytest', "Framework :: Pytest",
'Intended Audience :: Developers', "Intended Audience :: Developers",
'License :: OSI Approved :: MIT License', "License :: OSI Approved :: MIT License",
'Operating System :: POSIX', "Operating System :: POSIX",
'Operating System :: Microsoft :: Windows', "Operating System :: Microsoft :: Windows",
'Operating System :: MacOS :: MacOS X', "Operating System :: MacOS :: MacOS X",
'Topic :: Software Development :: Testing', "Topic :: Software Development :: Testing",
'Topic :: Software Development :: Quality Assurance', "Topic :: Software Development :: Quality Assurance",
'Topic :: Utilities', "Topic :: Utilities",
'Programming Language :: Python', "Programming Language :: Python",
'Programming Language :: Python :: 2', "Programming Language :: Python :: 2",
'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.4",
'Programming Language :: Python :: 3.5', "Programming Language :: Python :: 3.5",
'Programming Language :: Python :: 3.6', "Programming Language :: Python :: 3.6",
], ],
) )

File diff suppressed because it is too large Load Diff

View File

@@ -20,6 +20,7 @@ pytest_plugins = "pytester"
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def _divert_atexit(request, monkeypatch): def _divert_atexit(request, monkeypatch):
import atexit import atexit
finalizers = [] finalizers = []
def finish(): def finish():
@@ -31,10 +32,12 @@ def _divert_atexit(request, monkeypatch):
def pytest_addoption(parser): def pytest_addoption(parser):
parser.addoption('--gx', parser.addoption(
action="append", "--gx",
dest="gspecs", action="append",
help="add a global test environment, XSpec-syntax. ") dest="gspecs",
help="add a global test environment, XSpec-syntax. ",
)
@pytest.fixture @pytest.fixture

View File

@@ -1,9 +1,6 @@
from xdist.dsession import DSession from xdist.dsession import DSession
from xdist.report import report_collection_diff from xdist.report import report_collection_diff
from xdist.scheduler import ( from xdist.scheduler import EachScheduling, LoadScheduling
EachScheduling,
LoadScheduling,
)
import py import py
import pytest import pytest
@@ -60,7 +57,7 @@ class TestEachScheduling:
sched = EachScheduling(config) sched = EachScheduling(config)
sched.add_node(node1) sched.add_node(node1)
sched.add_node(node2) sched.add_node(node2)
collection = ["a.py::test_1", ] collection = ["a.py::test_1"]
assert not sched.collection_is_completed assert not sched.collection_is_completed
sched.add_node_collection(node1, collection) sched.add_node_collection(node1, collection)
assert not sched.collection_is_completed assert not sched.collection_is_completed
@@ -70,8 +67,8 @@ class TestEachScheduling:
assert sched.node2collection[node2] == collection assert sched.node2collection[node2] == collection
sched.schedule() sched.schedule()
assert sched.tests_finished assert sched.tests_finished
assert node1.sent == ['ALL'] assert node1.sent == ["ALL"]
assert node2.sent == ['ALL'] assert node2.sent == ["ALL"]
sched.mark_test_complete(node1, 0) sched.mark_test_complete(node1, 0)
assert sched.tests_finished assert sched.tests_finished
sched.mark_test_complete(node2, 0) sched.mark_test_complete(node2, 0)
@@ -82,7 +79,7 @@ class TestEachScheduling:
config = testdir.parseconfig("--tx=popen") config = testdir.parseconfig("--tx=popen")
sched = EachScheduling(config) sched = EachScheduling(config)
sched.add_node(node1) sched.add_node(node1)
collection = ["a.py::test_1", ] collection = ["a.py::test_1"]
assert not sched.collection_is_completed assert not sched.collection_is_completed
sched.add_node_collection(node1, collection) sched.add_node_collection(node1, collection)
assert sched.collection_is_completed assert sched.collection_is_completed
@@ -203,7 +200,7 @@ class TestLoadScheduling:
def test_different_tests_collected(self, testdir): def test_different_tests_collected(self, testdir):
""" """
Test that LoadScheduling is reporting collection errors when Test that LoadScheduling is reporting collection errors when
different test ids are collected by slaves. different test ids are collected by workers.
""" """
class CollectHook(object): class CollectHook(object):
@@ -230,7 +227,7 @@ class TestLoadScheduling:
sched.schedule() sched.schedule()
assert len(collect_hook.reports) == 1 assert len(collect_hook.reports) == 1
rep = collect_hook.reports[0] rep = collect_hook.reports[0]
assert 'Different tests were collected between' in rep.longrepr assert "Different tests were collected between" in rep.longrepr
class TestDistReporter: class TestDistReporter:
@@ -238,6 +235,7 @@ class TestDistReporter:
def test_rsync_printing(self, testdir, linecomp): def test_rsync_printing(self, testdir, linecomp):
config = testdir.parseconfig() config = testdir.parseconfig()
from _pytest.pytest_terminal import TerminalReporter from _pytest.pytest_terminal import TerminalReporter
rep = TerminalReporter(config, file=linecomp.stringio) rep = TerminalReporter(config, file=linecomp.stringio)
config.pluginmanager.register(rep, "terminalreporter") config.pluginmanager.register(rep, "terminalreporter")
dsession = DSession(config) dsession = DSession(config)
@@ -249,6 +247,7 @@ class TestDistReporter:
class gw2: class gw2:
id = "X2" id = "X2"
spec = execnet.XSpec("popen") spec = execnet.XSpec("popen")
# class rinfo: # class rinfo:
# version_info = (2, 5, 1, 'final', 0) # version_info = (2, 5, 1, 'final', 0)
# executable = "hello" # executable = "hello"
@@ -260,47 +259,50 @@ class TestDistReporter:
# "*X1*popen*xyz*2.5*" # "*X1*popen*xyz*2.5*"
# ]) # ])
dsession.pytest_xdist_rsyncstart(source="hello", gateways=[gw1, gw2]) dsession.pytest_xdist_rsyncstart(source="hello", gateways=[gw1, gw2])
linecomp.assert_contains_lines(["[X1,X2] rsyncing: hello", ]) linecomp.assert_contains_lines(["[X1,X2] rsyncing: hello"])
def test_report_collection_diff_equal(): def test_report_collection_diff_equal():
"""Test reporting of equal collections.""" """Test reporting of equal collections."""
from_collection = to_collection = ['aaa', 'bbb', 'ccc'] from_collection = to_collection = ["aaa", "bbb", "ccc"]
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_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"]
to_collection = ['aZa', 'bbb', 'XXX', 'ccc'] to_collection = ["aZa", "bbb", "XXX", "ccc"]
error_message = ( error_message = (
'Different tests were collected between 1 and 2. The difference is:\n' "Different tests were collected between 1 and 2. The difference is:\n"
'--- 1\n' "--- 1\n"
'\n' "\n"
'+++ 2\n' "+++ 2\n"
'\n' "\n"
'@@ -1,4 +1,4 @@\n' "@@ -1,4 +1,4 @@\n"
'\n' "\n"
'-aaa\n' "-aaa\n"
'+aZa\n' "+aZa\n"
' bbb\n' " bbb\n"
'+XXX\n' "+XXX\n"
' ccc\n' " ccc\n"
'-YYY') "-YYY"
)
msg = report_collection_diff(from_collection, to_collection, '1', '2') msg = report_collection_diff(from_collection, to_collection, "1", "2")
assert msg == error_message assert msg == error_message
@pytest.mark.xfail(reason="duplicate test ids not supported yet") @pytest.mark.xfail(reason="duplicate test ids not supported yet")
def test_pytest_issue419(testdir): def test_pytest_issue419(testdir):
testdir.makepyfile(""" testdir.makepyfile(
"""
import pytest import pytest
@pytest.mark.parametrize('birth_year', [1988, 1988, ]) @pytest.mark.parametrize('birth_year', [1988, 1988, ])
def test_2011_table(birth_year): def test_2011_table(birth_year):
pass pass
""") """
)
reprec = testdir.inline_run("-n1") reprec = testdir.inline_run("-n1")
reprec.assertoutcome(passed=2) reprec.assertoutcome(passed=2)
assert 0 assert 0

View File

@@ -60,7 +60,7 @@ class TestStatRecorder:
p.remove() p.remove()
# make check()'s visit() call return our just removed # make check()'s visit() call return our just removed
# path as if we were in a race condition # path as if we were in a race condition
monkeypatch.setattr(tmp, 'visit', lambda *args: [p]) monkeypatch.setattr(tmp, "visit", lambda *args: [p])
changed = sd.check() changed = sd.check()
assert changed assert changed
@@ -84,7 +84,7 @@ class TestStatRecorder:
sd = StatRecorder([tmp]) sd = StatRecorder([tmp])
ret_values = [True, False] ret_values = [True, False]
monkeypatch.setattr(StatRecorder, 'check', lambda self: ret_values.pop()) monkeypatch.setattr(StatRecorder, "check", lambda self: ret_values.pop())
sd.waitonchange(checkinterval=0.2) sd.waitonchange(checkinterval=0.2)
assert not ret_values assert not ret_values
@@ -110,19 +110,25 @@ class TestRemoteControl:
assert not failures assert not failures
def test_failure_change(self, testdir): def test_failure_change(self, testdir):
modcol = testdir.getitem(""" modcol = testdir.getitem(
"""
def test_func(): def test_func():
assert 0 assert 0
""") """
)
control = RemoteControl(modcol.config) control = RemoteControl(modcol.config)
control.loop_once() control.loop_once()
assert control.failures assert control.failures
modcol.fspath.write(py.code.Source(""" modcol.fspath.write(
py.code.Source(
"""
def test_func(): def test_func():
assert 1 assert 1
def test_new(): def test_new():
assert 0 assert 0
""")) """
)
)
removepyc(modcol.fspath) removepyc(modcol.fspath)
control.loop_once() control.loop_once()
assert not control.failures assert not control.failures
@@ -131,14 +137,17 @@ class TestRemoteControl:
assert str(control.failures).find("test_new") != -1 assert str(control.failures).find("test_new") != -1
def test_failure_subdir_no_init(self, testdir): def test_failure_subdir_no_init(self, testdir):
modcol = testdir.getitem(""" modcol = testdir.getitem(
"""
def test_func(): def test_func():
assert 0 assert 0
""") """
)
parent = modcol.fspath.dirpath().dirpath() parent = modcol.fspath.dirpath().dirpath()
parent.chdir() parent.chdir()
modcol.config.args = [py.path.local(x).relto(parent) modcol.config.args = [
for x in modcol.config.args] py.path.local(x).relto(parent) for x in modcol.config.args
]
control = RemoteControl(modcol.config) control = RemoteControl(modcol.config)
control.loop_once() control.loop_once()
assert control.failures assert control.failures
@@ -148,70 +157,87 @@ class TestRemoteControl:
class TestLooponFailing: class TestLooponFailing:
def test_looponfail_from_fail_to_ok(self, testdir): def test_looponfail_from_fail_to_ok(self, testdir):
modcol = testdir.getmodulecol(""" modcol = testdir.getmodulecol(
"""
def test_one(): def test_one():
x = 0 x = 0
assert x == 1 assert x == 1
def test_two(): def test_two():
assert 1 assert 1
""") """
)
remotecontrol = RemoteControl(modcol.config) remotecontrol = RemoteControl(modcol.config)
remotecontrol.loop_once() remotecontrol.loop_once()
assert len(remotecontrol.failures) == 1 assert len(remotecontrol.failures) == 1
modcol.fspath.write(py.code.Source(""" modcol.fspath.write(
py.code.Source(
"""
def test_one(): def test_one():
assert 1 assert 1
def test_two(): def test_two():
assert 1 assert 1
""")) """
)
)
removepyc(modcol.fspath) removepyc(modcol.fspath)
remotecontrol.loop_once() remotecontrol.loop_once()
assert not remotecontrol.failures assert not remotecontrol.failures
def test_looponfail_from_one_to_two_tests(self, testdir): def test_looponfail_from_one_to_two_tests(self, testdir):
modcol = testdir.getmodulecol(""" modcol = testdir.getmodulecol(
"""
def test_one(): def test_one():
assert 0 assert 0
""") """
)
remotecontrol = RemoteControl(modcol.config) remotecontrol = RemoteControl(modcol.config)
remotecontrol.loop_once() remotecontrol.loop_once()
assert len(remotecontrol.failures) == 1 assert len(remotecontrol.failures) == 1
assert 'test_one' in remotecontrol.failures[0] assert "test_one" in remotecontrol.failures[0]
modcol.fspath.write(py.code.Source(""" modcol.fspath.write(
py.code.Source(
"""
def test_one(): def test_one():
assert 1 # passes now assert 1 # passes now
def test_two(): def test_two():
assert 0 # new and fails assert 0 # new and fails
""")) """
)
)
removepyc(modcol.fspath) removepyc(modcol.fspath)
remotecontrol.loop_once() remotecontrol.loop_once()
assert len(remotecontrol.failures) == 0 assert len(remotecontrol.failures) == 0
remotecontrol.loop_once() remotecontrol.loop_once()
assert len(remotecontrol.failures) == 1 assert len(remotecontrol.failures) == 1
assert 'test_one' not in remotecontrol.failures[0] assert "test_one" not in remotecontrol.failures[0]
assert 'test_two' in remotecontrol.failures[0] assert "test_two" in remotecontrol.failures[0]
@py.test.mark.xfail(py.test.__version__ >= "3.1", @py.test.mark.xfail(py.test.__version__ >= "3.1", reason="broken by pytest 3.1+")
reason="broken by pytest 3.1+")
def test_looponfail_removed_test(self, testdir): def test_looponfail_removed_test(self, testdir):
modcol = testdir.getmodulecol(""" modcol = testdir.getmodulecol(
"""
def test_one(): def test_one():
assert 0 assert 0
def test_two(): def test_two():
assert 0 assert 0
""") """
)
remotecontrol = RemoteControl(modcol.config) remotecontrol = RemoteControl(modcol.config)
remotecontrol.loop_once() remotecontrol.loop_once()
assert len(remotecontrol.failures) == 2 assert len(remotecontrol.failures) == 2
modcol.fspath.write(py.code.Source(""" modcol.fspath.write(
py.code.Source(
"""
def test_xxx(): # renamed test def test_xxx(): # renamed test
assert 0 assert 0
def test_two(): def test_two():
assert 1 # pass now assert 1 # pass now
""")) """
)
)
removepyc(modcol.fspath) removepyc(modcol.fspath)
remotecontrol.loop_once() remotecontrol.loop_once()
assert len(remotecontrol.failures) == 0 assert len(remotecontrol.failures) == 0
@@ -220,10 +246,12 @@ class TestLooponFailing:
assert len(remotecontrol.failures) == 1 assert len(remotecontrol.failures) == 1
def test_looponfail_multiple_errors(self, testdir, monkeypatch): def test_looponfail_multiple_errors(self, testdir, monkeypatch):
modcol = testdir.getmodulecol(""" modcol = testdir.getmodulecol(
"""
def test_one(): def test_one():
assert 0 assert 0
""") """
)
remotecontrol = RemoteControl(modcol.config) remotecontrol = RemoteControl(modcol.config)
orig_runsession = remotecontrol.runsession orig_runsession = remotecontrol.runsession
@@ -233,18 +261,20 @@ class TestLooponFailing:
print(failures) print(failures)
return failures * 2, reports, collection_failed return failures * 2, reports, collection_failed
monkeypatch.setattr(remotecontrol, 'runsession', runsession_dups) monkeypatch.setattr(remotecontrol, "runsession", runsession_dups)
remotecontrol.loop_once() remotecontrol.loop_once()
assert len(remotecontrol.failures) == 1 assert len(remotecontrol.failures) == 1
class TestFunctional: class TestFunctional:
def test_fail_to_ok(self, testdir): def test_fail_to_ok(self, testdir):
p = testdir.makepyfile(""" p = testdir.makepyfile(
"""
def test_one(): def test_one():
x = 0 x = 0
assert x == 1 assert x == 1
""") """
)
# 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)
@@ -253,21 +283,27 @@ class TestFunctional:
child.expect("1 failed") child.expect("1 failed")
child.expect("### LOOPONFAILING ####") child.expect("### LOOPONFAILING ####")
child.expect("waiting for changes") child.expect("waiting for changes")
p.write(py.code.Source(""" p.write(
py.code.Source(
"""
def test_one(): def test_one():
x = 1 x = 1
assert x == 1 assert x == 1
""")) """
)
)
child.expect(".*1 passed.*") child.expect(".*1 passed.*")
child.kill(15) child.kill(15)
def test_xfail_passes(self, testdir): def test_xfail_passes(self, testdir):
p = testdir.makepyfile(""" p = testdir.makepyfile(
"""
import py import py
@py.test.mark.xfail @py.test.mark.xfail
def test_one(): def test_one():
pass pass
""") """
)
child = testdir.spawn_pytest("-f %s" % p) child = testdir.spawn_pytest("-f %s" % p)
child.expect("1 xpass") child.expect("1 xpass")
# child.expect("### LOOPONFAILING ####") # child.expect("### LOOPONFAILING ####")

View File

@@ -2,54 +2,58 @@ import pytest
class TestHooks: class TestHooks:
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def create_test_file(self, testdir): def create_test_file(self, testdir):
testdir.makepyfile(""" testdir.makepyfile(
"""
import os import os
def test_a(): pass def test_a(): pass
def test_b(): pass def test_b(): pass
def test_c(): pass def test_c(): pass
""") """
)
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" and "worker_id" attributes. (#8)
""" """
testdir.makeconftest(""" testdir.makeconftest(
"""
def pytest_runtest_logreport(report): def pytest_runtest_logreport(report):
if hasattr(report, 'node'): if hasattr(report, 'node'):
if report.when == "call": if report.when == "call":
slaveid = report.node.slaveinput['slaveid'] workerid = report.node.workerinput['workerid']
if slaveid != report.worker_id: if workerid != report.worker_id:
print("HOOK: Worker id mismatch: %s %s" print("HOOK: Worker id mismatch: %s %s"
% (slaveid, report.worker_id)) % (workerid, report.worker_id))
else: else:
print("HOOK: %s %s" print("HOOK: %s %s"
% (report.nodeid, report.worker_id)) % (report.nodeid, report.worker_id))
""") """
res = testdir.runpytest('-n1', '-s') )
res.stdout.fnmatch_lines([ res = testdir.runpytest("-n1", "-s")
'*HOOK: test_runtest_logreport.py::test_a gw0*', res.stdout.fnmatch_lines(
'*HOOK: test_runtest_logreport.py::test_b gw0*', [
'*HOOK: test_runtest_logreport.py::test_c gw0*', "*HOOK: test_runtest_logreport.py::test_a gw0*",
'*3 passed*', "*HOOK: test_runtest_logreport.py::test_b gw0*",
]) "*HOOK: test_runtest_logreport.py::test_c gw0*",
"*3 passed*",
]
)
def test_node_collection_finished(self, testdir): def test_node_collection_finished(self, testdir):
"""Test pytest_xdist_node_collection_finished hook (#8). """Test pytest_xdist_node_collection_finished hook (#8).
""" """
testdir.makeconftest(""" testdir.makeconftest(
"""
def pytest_xdist_node_collection_finished(node, ids): def pytest_xdist_node_collection_finished(node, ids):
slaveid = node.slaveinput['slaveid'] workerid = node.workerinput['workerid']
stripped_ids = [x.split('::')[1] for x in ids] stripped_ids = [x.split('::')[1] for x in ids]
print("HOOK: %s %s" % (slaveid, ', '.join(stripped_ids))) print("HOOK: %s %s" % (workerid, ', '.join(stripped_ids)))
""") """
res = testdir.runpytest('-n2', '-s') )
res.stdout.fnmatch_lines_random([ res = testdir.runpytest("-n2", "-s")
'*HOOK: gw0 test_a, test_b, test_c', res.stdout.fnmatch_lines_random(
'*HOOK: gw1 test_a, test_b, test_c', ["*HOOK: gw0 test_a, test_b, test_c", "*HOOK: gw1 test_a, test_b, test_c"]
]) )
res.stdout.fnmatch_lines([ res.stdout.fnmatch_lines(["*3 passed*"])
'*3 passed*',
])

View File

@@ -1,6 +1,6 @@
import py import py
import execnet import execnet
from xdist.slavemanage import NodeManager from xdist.workermanage import NodeManager
def test_dist_incompatibility_messages(testdir): def test_dist_incompatibility_messages(testdir):
@@ -16,14 +16,15 @@ def test_dist_incompatibility_messages(testdir):
def test_dist_options(testdir): def test_dist_options(testdir):
from xdist.plugin import pytest_cmdline_main as check_options from xdist.plugin import pytest_cmdline_main as check_options
config = testdir.parseconfigure("-n 2") config = testdir.parseconfigure("-n 2")
check_options(config) check_options(config)
assert config.option.dist == "load" assert config.option.dist == "load"
assert config.option.tx == ['popen'] * 2 assert config.option.tx == ["popen"] * 2
config = testdir.parseconfigure("--numprocesses", "2") config = testdir.parseconfigure("--numprocesses", "2")
check_options(config) check_options(config)
assert config.option.dist == "load" assert config.option.dist == "load"
assert config.option.tx == ['popen'] * 2 assert config.option.tx == ["popen"] * 2
config = testdir.parseconfigure("-d") config = testdir.parseconfigure("-d")
check_options(config) check_options(config)
assert config.option.dist == "load" assert config.option.dist == "load"
@@ -31,21 +32,31 @@ def test_dist_options(testdir):
def test_auto_detect_cpus(testdir, monkeypatch): def test_auto_detect_cpus(testdir, monkeypatch):
import os import os
if hasattr(os, 'cpu_count'):
monkeypatch.setattr(os, 'cpu_count', lambda: 99) if hasattr(os, "sched_getaffinity"):
monkeypatch.setattr(os, "sched_getaffinity", lambda _pid: set(range(99)))
elif hasattr(os, "cpu_count"):
monkeypatch.setattr(os, "cpu_count", lambda: 99)
else: else:
import multiprocessing import multiprocessing
monkeypatch.setattr(multiprocessing, 'cpu_count', lambda: 99)
monkeypatch.setattr(multiprocessing, "cpu_count", lambda: 99)
config = testdir.parseconfigure("-n2") config = testdir.parseconfigure("-n2")
assert config.getoption('numprocesses') == 2 assert config.getoption("numprocesses") == 2
config = testdir.parseconfigure("-nauto") config = testdir.parseconfigure("-nauto")
assert config.getoption('numprocesses') == 99 assert config.getoption("numprocesses") == 99
monkeypatch.delattr(os, "sched_getaffinity", raising=False)
monkeypatch.setenv("TRAVIS", "true")
config = testdir.parseconfigure("-nauto")
assert config.getoption("numprocesses") == 2
def test_boxed_with_collect_only(testdir): def test_boxed_with_collect_only(testdir):
from xdist.plugin import pytest_cmdline_main as check_options from xdist.plugin import pytest_cmdline_main as check_options
config = testdir.parseconfigure("-n1", "--boxed") config = testdir.parseconfigure("-n1", "--boxed")
check_options(config) check_options(config)
assert config.option.forked assert config.option.forked
@@ -85,13 +96,13 @@ class TestDistOptions:
assert xspecs[1].ssh == "xyz" assert xspecs[1].ssh == "xyz"
def test_xspecs_multiplied(self, testdir): def test_xspecs_multiplied(self, testdir):
config = testdir.parseconfigure("--tx=3*popen", ) config = testdir.parseconfigure("--tx=3*popen")
xspecs = NodeManager(config)._getxspecs() xspecs = NodeManager(config)._getxspecs()
assert len(xspecs) == 3 assert len(xspecs) == 3
assert xspecs[1].popen assert xspecs[1].popen
def test_getrsyncdirs(self, testdir): def test_getrsyncdirs(self, testdir):
config = testdir.parseconfigure('--rsyncdir=' + str(testdir.tmpdir)) config = testdir.parseconfigure("--rsyncdir=" + str(testdir.tmpdir))
nm = NodeManager(config, specs=[execnet.XSpec("popen")]) nm = NodeManager(config, specs=[execnet.XSpec("popen")])
assert not nm._getrsyncdirs() assert not nm._getrsyncdirs()
nm = NodeManager(config, specs=[execnet.XSpec("popen//chdir=qwe")]) nm = NodeManager(config, specs=[execnet.XSpec("popen//chdir=qwe")])
@@ -99,23 +110,24 @@ class TestDistOptions:
assert testdir.tmpdir in nm.roots assert testdir.tmpdir in nm.roots
def test_getrsyncignore(self, testdir): def test_getrsyncignore(self, testdir):
config = testdir.parseconfigure('--rsyncignore=fo*') config = testdir.parseconfigure("--rsyncignore=fo*")
nm = NodeManager(config, specs=[execnet.XSpec("popen//chdir=qwe")]) nm = NodeManager(config, specs=[execnet.XSpec("popen//chdir=qwe")])
assert 'fo*' in nm.rsyncoptions['ignores'] assert "fo*" in nm.rsyncoptions["ignores"]
def test_getrsyncdirs_with_conftest(self, testdir): def test_getrsyncdirs_with_conftest(self, testdir):
p = py.path.local() p = py.path.local()
for bn in 'x y z'.split(): for bn in "x y z".split():
p.mkdir(bn) p.mkdir(bn)
testdir.makeini(""" testdir.makeini(
"""
[pytest] [pytest]
rsyncdirs= x rsyncdirs= x
""") """
config = testdir.parseconfigure( )
testdir.tmpdir, '--rsyncdir=y', '--rsyncdir=z') config = testdir.parseconfigure(testdir.tmpdir, "--rsyncdir=y", "--rsyncdir=z")
nm = NodeManager(config, specs=[execnet.XSpec("popen//chdir=xyz")]) nm = NodeManager(config, specs=[execnet.XSpec("popen//chdir=xyz")])
roots = nm._getrsyncdirs() roots = nm._getrsyncdirs()
# assert len(roots) == 3 + 1 # pylib # assert len(roots) == 3 + 1 # pylib
assert py.path.local('y') in roots assert py.path.local("y") in roots
assert py.path.local('z') in roots assert py.path.local("z") in roots
assert testdir.tmpdir.join('x') in roots assert testdir.tmpdir.join("x") in roots

View File

@@ -1,11 +1,12 @@
import py import py
import pprint
import pytest import pytest
from xdist.slavemanage import SlaveController, unserialize_report from xdist.workermanage import WorkerController, unserialize_report
from xdist.remote import serialize_report from xdist.remote import serialize_report
import execnet import execnet
import marshal import marshal
queue = py.builtin._tryimport("queue", "Queue") from six.moves.queue import Queue
WAIT_TIMEOUT = 10.0 WAIT_TIMEOUT = 10.0
@@ -14,7 +15,7 @@ def check_marshallable(d):
try: try:
marshal.dumps(d) marshal.dumps(d)
except ValueError: except ValueError:
py.std.pprint.pprint(d) pprint.pprint(d)
raise ValueError("not marshallable") raise ValueError("not marshallable")
@@ -26,15 +27,15 @@ class EventCall:
return "<EventCall %s(**%s)>" % (self.name, self.kwargs) return "<EventCall %s(**%s)>" % (self.name, self.kwargs)
class SlaveSetup: class WorkerSetup:
use_callback = False use_callback = False
def __init__(self, request, testdir): def __init__(self, request, testdir):
self.request = request self.request = request
self.testdir = testdir self.testdir = testdir
self.events = queue.Queue() self.events = Queue()
def setup(self, ): def setup(self,):
self.testdir.chdir() self.testdir.chdir()
# import os ; os.environ['EXECNET_DEBUG'] = "2" # import os ; os.environ['EXECNET_DEBUG'] = "2"
self.gateway = execnet.makegateway() self.gateway = execnet.makegateway()
@@ -44,8 +45,7 @@ class SlaveSetup:
class DummyMananger: class DummyMananger:
specs = [0, 1] specs = [0, 1]
self.slp = SlaveController(DummyMananger, self.gateway, config, self.slp = WorkerController(DummyMananger, self.gateway, config, putevent)
putevent)
self.request.addfinalizer(self.slp.ensure_teardown) self.request.addfinalizer(self.slp.ensure_teardown)
self.slp.setup() self.slp.setup()
@@ -58,20 +58,21 @@ class SlaveSetup:
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 %s" % (ev,))
def sendcommand(self, name, **kwargs): def sendcommand(self, name, **kwargs):
self.slp.sendcommand(name, **kwargs) self.slp.sendcommand(name, **kwargs)
@pytest.fixture @pytest.fixture
def slave(request, testdir): def worker(request, testdir):
return SlaveSetup(request, testdir) return WorkerSetup(request, testdir)
@pytest.mark.xfail(reason='#59') @pytest.mark.xfail(reason="#59")
def test_remoteinitconfig(testdir): def test_remoteinitconfig(testdir):
from xdist.remote import remote_initconfig from xdist.remote import remote_initconfig
config1 = testdir.parseconfig() config1 = testdir.parseconfig()
config2 = remote_initconfig(config1.option.__dict__, config1.args) config2 = remote_initconfig(config1.option.__dict__, config1.args)
assert config2.option.__dict__ == config1.option.__dict__ assert config2.option.__dict__ == config1.option.__dict__
@@ -80,29 +81,33 @@ def test_remoteinitconfig(testdir):
class TestReportSerialization: class TestReportSerialization:
def test_xdist_longrepr_to_str_issue_241(self, testdir): def test_xdist_longrepr_to_str_issue_241(self, testdir):
testdir.makepyfile(""" testdir.makepyfile(
"""
import os import os
def test_a(): assert False def test_a(): assert False
def test_b(): pass def test_b(): pass
""") """
testdir.makeconftest(""" )
testdir.makeconftest(
"""
def pytest_runtest_logreport(report): def pytest_runtest_logreport(report):
print(report.longrepr) print(report.longrepr)
""") """
res = testdir.runpytest('-n1', '-s') )
res.stdout.fnmatch_lines([ res = testdir.runpytest("-n1", "-s")
'*1 failed, 1 passed *' res.stdout.fnmatch_lines(["*1 failed, 1 passed *"])
])
def test_xdist_report_longrepr_reprcrash_130(self, testdir): def test_xdist_report_longrepr_reprcrash_130(self, testdir):
reprec = testdir.inline_runsource(""" reprec = testdir.inline_runsource(
"""
import py import py
def test_fail(): assert False, 'Expected Message' def test_fail(): assert False, 'Expected Message'
""") """
)
reports = reprec.getreports("pytest_runtest_logreport") reports = reprec.getreports("pytest_runtest_logreport")
assert len(reports) == 3 assert len(reports) == 3
rep = reports[1] rep = reports[1]
added_section = ('Failure Metadata', str("metadata metadata"), "*") added_section = ("Failure Metadata", str("metadata metadata"), "*")
rep.longrepr.sections.append(added_section) rep.longrepr.sections.append(added_section)
d = serialize_report(rep) d = serialize_report(rep)
check_marshallable(d) check_marshallable(d)
@@ -110,28 +115,31 @@ class TestReportSerialization:
# Check assembled == rep # Check assembled == rep
assert a.__dict__.keys() == rep.__dict__.keys() assert a.__dict__.keys() == rep.__dict__.keys()
for key in rep.__dict__.keys(): for key in rep.__dict__.keys():
if key != 'longrepr': if key != "longrepr":
assert getattr(a, key) == getattr(rep, key) assert getattr(a, key) == getattr(rep, key)
assert rep.longrepr.reprcrash.lineno == a.longrepr.reprcrash.lineno assert rep.longrepr.reprcrash.lineno == a.longrepr.reprcrash.lineno
assert rep.longrepr.reprcrash.message == a.longrepr.reprcrash.message assert rep.longrepr.reprcrash.message == a.longrepr.reprcrash.message
assert rep.longrepr.reprcrash.path == a.longrepr.reprcrash.path assert rep.longrepr.reprcrash.path == a.longrepr.reprcrash.path
assert rep.longrepr.reprtraceback.entrysep \ assert rep.longrepr.reprtraceback.entrysep == a.longrepr.reprtraceback.entrysep
== a.longrepr.reprtraceback.entrysep assert (
assert rep.longrepr.reprtraceback.extraline \ rep.longrepr.reprtraceback.extraline == a.longrepr.reprtraceback.extraline
== a.longrepr.reprtraceback.extraline )
assert rep.longrepr.reprtraceback.style \ assert rep.longrepr.reprtraceback.style == a.longrepr.reprtraceback.style
== a.longrepr.reprtraceback.style
assert rep.longrepr.sections == a.longrepr.sections assert rep.longrepr.sections == a.longrepr.sections
# Missing section attribute PR171 # Missing section attribute PR171
assert added_section in a.longrepr.sections assert added_section in a.longrepr.sections
def test_reprentries_serialization_170(self, testdir): def test_reprentries_serialization_170(self, testdir):
from _pytest._code.code import ReprEntry from _pytest._code.code import ReprEntry
reprec = testdir.inline_runsource("""
reprec = testdir.inline_runsource(
"""
def test_repr_entry(): def test_repr_entry():
x = 0 x = 0
assert x assert x
""", '--showlocals') """,
"--showlocals",
)
reports = reprec.getreports("pytest_runtest_logreport") reports = reprec.getreports("pytest_runtest_logreport")
assert len(reports) == 3 assert len(reports) == 3
rep = reports[1] rep = reports[1]
@@ -145,7 +153,9 @@ class TestReportSerialization:
assert rep_entries[i].lines == a_entries[i].lines assert rep_entries[i].lines == a_entries[i].lines
assert rep_entries[i].localssep == a_entries[i].localssep assert rep_entries[i].localssep == a_entries[i].localssep
assert rep_entries[i].reprfileloc.lineno == a_entries[i].reprfileloc.lineno assert rep_entries[i].reprfileloc.lineno == a_entries[i].reprfileloc.lineno
assert rep_entries[i].reprfileloc.message == a_entries[i].reprfileloc.message assert (
rep_entries[i].reprfileloc.message == a_entries[i].reprfileloc.message
)
assert rep_entries[i].reprfileloc.path == a_entries[i].reprfileloc.path assert rep_entries[i].reprfileloc.path == a_entries[i].reprfileloc.path
assert rep_entries[i].reprfuncargs.args == a_entries[i].reprfuncargs.args assert rep_entries[i].reprfuncargs.args == a_entries[i].reprfuncargs.args
assert rep_entries[i].reprlocals.lines == a_entries[i].reprlocals.lines assert rep_entries[i].reprlocals.lines == a_entries[i].reprlocals.lines
@@ -153,11 +163,15 @@ class TestReportSerialization:
def test_reprentries_serialization_196(self, testdir): def test_reprentries_serialization_196(self, testdir):
from _pytest._code.code import ReprEntryNative from _pytest._code.code import ReprEntryNative
reprec = testdir.inline_runsource("""
reprec = testdir.inline_runsource(
"""
def test_repr_entry_native(): def test_repr_entry_native():
x = 0 x = 0
assert x assert x
""", '--tb=native') """,
"--tb=native",
)
reports = reprec.getreports("pytest_runtest_logreport") reports = reprec.getreports("pytest_runtest_logreport")
assert len(reports) == 3 assert len(reports) == 3
rep = reports[1] rep = reports[1]
@@ -171,7 +185,8 @@ class TestReportSerialization:
assert rep_entries[i].lines == a_entries[i].lines assert rep_entries[i].lines == a_entries[i].lines
def test_itemreport_outcomes(self, testdir): def test_itemreport_outcomes(self, testdir):
reprec = testdir.inline_runsource(""" reprec = testdir.inline_runsource(
"""
import py import py
def test_pass(): pass def test_pass(): pass
def test_fail(): 0/0 def test_fail(): 0/0
@@ -183,7 +198,8 @@ class TestReportSerialization:
def test_xfail(): 0/0 def test_xfail(): 0/0
def test_xfail_imperative(): def test_xfail_imperative():
py.test.xfail("hello") py.test.xfail("hello")
""") """
)
reports = reprec.getreports("pytest_runtest_logreport") reports = reprec.getreports("pytest_runtest_logreport")
assert len(reports) == 17 # with setup/teardown "passed" reports assert len(reports) == 17 # with setup/teardown "passed" reports
for rep in reports: for rep in reports:
@@ -243,133 +259,144 @@ class TestReportSerialization:
assert newrep.longrepr == str(rep.longrepr) assert newrep.longrepr == str(rep.longrepr)
class TestSlaveInteractor: class TestWorkerInteractor:
def test_basic_collect_and_runtests(self, slave): def test_basic_collect_and_runtests(self, worker):
slave.testdir.makepyfile(""" worker.testdir.makepyfile(
"""
def test_func(): def test_func():
pass pass
""") """
slave.setup() )
ev = slave.popevent() worker.setup()
assert ev.name == "slaveready" ev = worker.popevent()
ev = slave.popevent() assert ev.name == "workerready"
ev = worker.popevent()
assert ev.name == "collectionstart" assert ev.name == "collectionstart"
assert not ev.kwargs assert not ev.kwargs
ev = slave.popevent("collectionfinish") ev = worker.popevent("collectionfinish")
assert ev.kwargs['topdir'] == slave.testdir.tmpdir assert ev.kwargs["topdir"] == worker.testdir.tmpdir
ids = ev.kwargs['ids'] ids = ev.kwargs["ids"]
assert len(ids) == 1 assert len(ids) == 1
slave.sendcommand("runtests", indices=list(range(len(ids)))) worker.sendcommand("runtests", indices=list(range(len(ids))))
slave.sendcommand("shutdown") worker.sendcommand("shutdown")
ev = slave.popevent("logstart") ev = worker.popevent("logstart")
assert ev.kwargs["nodeid"].endswith("test_func") assert ev.kwargs["nodeid"].endswith("test_func")
assert len(ev.kwargs["location"]) == 3 assert len(ev.kwargs["location"]) == 3
ev = slave.popevent("testreport") # setup ev = worker.popevent("testreport") # setup
ev = slave.popevent("testreport") ev = worker.popevent("testreport")
assert ev.name == "testreport" assert ev.name == "testreport"
rep = unserialize_report(ev.name, ev.kwargs['data']) rep = unserialize_report(ev.name, ev.kwargs["data"])
assert rep.nodeid.endswith("::test_func") assert rep.nodeid.endswith("::test_func")
assert rep.passed assert rep.passed
assert rep.when == "call" assert rep.when == "call"
ev = slave.popevent("slavefinished") ev = worker.popevent("workerfinished")
assert 'slaveoutput' in ev.kwargs assert "workeroutput" in ev.kwargs
@pytest.mark.skipif(pytest.__version__ >= '3.0', @pytest.mark.skipif(
reason='skip at module level illegal in pytest 3.0') pytest.__version__ >= "3.0", reason="skip at module level illegal in pytest 3.0"
def test_remote_collect_skip(self, slave): )
slave.testdir.makepyfile(""" def test_remote_collect_skip(self, worker):
worker.testdir.makepyfile(
"""
import py import py
py.test.skip("hello") py.test.skip("hello")
""") """
slave.setup() )
ev = slave.popevent("collectionstart") worker.setup()
ev = worker.popevent("collectionstart")
assert not ev.kwargs assert not ev.kwargs
ev = slave.popevent() ev = worker.popevent()
assert ev.name == "collectreport" assert ev.name == "collectreport"
ev = slave.popevent() ev = worker.popevent()
assert ev.name == "collectreport" assert ev.name == "collectreport"
rep = unserialize_report(ev.name, ev.kwargs['data']) rep = unserialize_report(ev.name, ev.kwargs["data"])
assert rep.skipped assert rep.skipped
ev = slave.popevent("collectionfinish") ev = worker.popevent("collectionfinish")
assert not ev.kwargs['ids'] assert not ev.kwargs["ids"]
def test_remote_collect_fail(self, slave): def test_remote_collect_fail(self, worker):
slave.testdir.makepyfile("""aasd qwe""") worker.testdir.makepyfile("""aasd qwe""")
slave.setup() worker.setup()
ev = slave.popevent("collectionstart") ev = worker.popevent("collectionstart")
assert not ev.kwargs assert not ev.kwargs
ev = slave.popevent() ev = worker.popevent()
assert ev.name == "collectreport" assert ev.name == "collectreport"
ev = slave.popevent() rep = unserialize_report(ev.name, ev.kwargs["data"])
assert ev.name == "collectreport"
rep = unserialize_report(ev.name, ev.kwargs['data'])
assert rep.failed assert rep.failed
ev = slave.popevent("collectionfinish") ev = worker.popevent("collectionfinish")
assert not ev.kwargs['ids'] assert not ev.kwargs["ids"]
def test_runtests_all(self, slave): def test_runtests_all(self, worker):
slave.testdir.makepyfile(""" worker.testdir.makepyfile(
"""
def test_func(): pass def test_func(): pass
def test_func2(): pass def test_func2(): pass
""") """
slave.setup() )
ev = slave.popevent() worker.setup()
assert ev.name == "slaveready" ev = worker.popevent()
ev = slave.popevent() assert ev.name == "workerready"
ev = worker.popevent()
assert ev.name == "collectionstart" assert ev.name == "collectionstart"
assert not ev.kwargs assert not ev.kwargs
ev = slave.popevent("collectionfinish") ev = worker.popevent("collectionfinish")
ids = ev.kwargs['ids'] ids = ev.kwargs["ids"]
assert len(ids) == 2 assert len(ids) == 2
slave.sendcommand("runtests_all", ) worker.sendcommand("runtests_all")
slave.sendcommand("shutdown", ) worker.sendcommand("shutdown")
for func in "::test_func", "::test_func2": for func in "::test_func", "::test_func2":
for i in range(3): # setup/call/teardown for i in range(3): # setup/call/teardown
ev = slave.popevent("testreport") ev = worker.popevent("testreport")
assert ev.name == "testreport" assert ev.name == "testreport"
rep = unserialize_report(ev.name, ev.kwargs['data']) rep = unserialize_report(ev.name, ev.kwargs["data"])
assert rep.nodeid.endswith(func) assert rep.nodeid.endswith(func)
ev = slave.popevent("slavefinished") ev = worker.popevent("workerfinished")
assert 'slaveoutput' in ev.kwargs assert "workeroutput" in ev.kwargs
def test_happy_run_events_converted(self, testdir, slave): def test_happy_run_events_converted(self, testdir, worker):
py.test.xfail("implement a simple test for event production") py.test.xfail("implement a simple test for event production")
assert not slave.use_callback assert not worker.use_callback
slave.testdir.makepyfile(""" worker.testdir.makepyfile(
"""
def test_func(): def test_func():
pass pass
""") """
slave.setup() )
hookrec = testdir.getreportrecorder(slave.config) worker.setup()
for data in slave.slp.channel: hookrec = testdir.getreportrecorder(worker.config)
slave.slp.process_from_remote(data) for data in worker.slp.channel:
slave.slp.process_from_remote(slave.slp.ENDMARK) worker.slp.process_from_remote(data)
py.std.pprint.pprint(hookrec.hookrecorder.calls) worker.slp.process_from_remote(worker.slp.ENDMARK)
hookrec.hookrecorder.contains([ pprint.pprint(hookrec.hookrecorder.calls)
("pytest_collectstart", "collector.fspath == aaa"), hookrec.hookrecorder.contains(
("pytest_pycollect_makeitem", "name == 'test_func'"), [
("pytest_collectreport", "report.collector.fspath == aaa"), ("pytest_collectstart", "collector.fspath == aaa"),
("pytest_collectstart", "collector.fspath == bbb"), ("pytest_pycollect_makeitem", "name == 'test_func'"),
("pytest_pycollect_makeitem", "name == 'test_func'"), ("pytest_collectreport", "report.collector.fspath == aaa"),
("pytest_collectreport", "report.collector.fspath == bbb"), ("pytest_collectstart", "collector.fspath == bbb"),
]) ("pytest_pycollect_makeitem", "name == 'test_func'"),
("pytest_collectreport", "report.collector.fspath == bbb"),
]
)
def test_process_from_remote_error_handling(self, slave, capsys): def test_process_from_remote_error_handling(self, worker, capsys):
slave.use_callback = True worker.use_callback = True
slave.setup() worker.setup()
slave.slp.process_from_remote(('<nonono>', ())) worker.slp.process_from_remote(("<nonono>", ()))
out, err = capsys.readouterr() out, err = capsys.readouterr()
assert 'INTERNALERROR> ValueError: unknown event: <nonono>' in out assert "INTERNALERROR> ValueError: unknown event: <nonono>" in out
ev = slave.popevent() ev = worker.popevent()
assert ev.name == "errordown" assert ev.name == "errordown"
def test_remote_env_vars(testdir): def test_remote_env_vars(testdir):
testdir.makepyfile(''' testdir.makepyfile(
"""
import os import os
def test(): def test():
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'
''') """
result = testdir.runpytest('-n2', '--max-slave-restart=0') )
result = testdir.runpytest("-n2", "--max-worker-restart=0")
assert result.ret == 0 assert result.ret == 0

View File

@@ -1,9 +1,11 @@
from __future__ import print_function
import py import py
import pytest import pytest
import textwrap
import execnet import execnet
from _pytest.pytester import HookRecorder from _pytest.pytester import HookRecorder
from xdist import slavemanage, newhooks from xdist import workermanage, newhooks
from xdist.slavemanage import HostRSync, NodeManager from xdist.workermanage import HostRSync, NodeManager
pytest_plugins = "pytester" pytest_plugins = "pytester"
@@ -32,7 +34,7 @@ def mysetup(tmpdir):
@pytest.fixture @pytest.fixture
def slavecontroller(monkeypatch): def workercontroller(monkeypatch):
class MockController(object): class MockController(object):
def __init__(self, *args): def __init__(self, *args):
pass pass
@@ -40,7 +42,7 @@ def slavecontroller(monkeypatch):
def setup(self): def setup(self):
pass pass
monkeypatch.setattr(slavemanage, 'SlaveController', MockController) monkeypatch.setattr(workermanage, "WorkerController", MockController)
return MockController return MockController
@@ -56,8 +58,7 @@ class TestNodeManagerPopen:
for spec in NodeManager(config, specs, defaultchdir="abc").specs: for spec in NodeManager(config, specs, defaultchdir="abc").specs:
assert spec.chdir == "abc" assert spec.chdir == "abc"
def test_popen_makegateway_events(self, config, hookrecorder, def test_popen_makegateway_events(self, config, hookrecorder, workercontroller):
slavecontroller):
hm = NodeManager(config, ["popen"] * 2) hm = NodeManager(config, ["popen"] * 2)
hm.setup_nodes(None) hm.setup_nodes(None)
call = hookrecorder.popcall("pytest_xdist_setupnodes") call = hookrecorder.popcall("pytest_xdist_setupnodes")
@@ -72,7 +73,7 @@ class TestNodeManagerPopen:
hm.teardown_nodes() hm.teardown_nodes()
assert not len(hm.group) assert not len(hm.group)
def test_popens_rsync(self, config, mysetup, slavecontroller): def test_popens_rsync(self, config, mysetup, workercontroller):
source = mysetup.source source = mysetup.source
hm = NodeManager(config, ["popen"] * 2) hm = NodeManager(config, ["popen"] * 2)
hm.setup_nodes(None) hm.setup_nodes(None)
@@ -97,7 +98,7 @@ class TestNodeManagerPopen:
assert not len(hm.group) assert not len(hm.group)
assert "sys.path.insert" in gw.remote_exec.args[0] assert "sys.path.insert" in gw.remote_exec.args[0]
def test_rsync_popen_with_path(self, config, mysetup, slavecontroller): def test_rsync_popen_with_path(self, config, mysetup, workercontroller):
source, dest = mysetup.source, mysetup.dest source, dest = mysetup.source, mysetup.dest
hm = NodeManager(config, ["popen//chdir=%s" % dest] * 1) hm = NodeManager(config, ["popen//chdir=%s" % dest] * 1)
hm.setup_nodes(None) hm.setup_nodes(None)
@@ -106,15 +107,16 @@ class TestNodeManagerPopen:
for gw in hm.group: for gw in hm.group:
hm.rsync(gw, source, notify=lambda *args: notifications.append(args)) hm.rsync(gw, source, notify=lambda *args: notifications.append(args))
assert len(notifications) == 1 assert len(notifications) == 1
assert notifications[0] == ("rsyncrootready", hm.group['gw0'].spec, source) assert notifications[0] == ("rsyncrootready", hm.group["gw0"].spec, source)
hm.teardown_nodes() hm.teardown_nodes()
dest = dest.join(source.basename) dest = dest.join(source.basename)
assert dest.join("dir1").check() assert dest.join("dir1").check()
assert dest.join("dir1", "dir2").check() assert dest.join("dir1", "dir2").check()
assert dest.join("dir1", "dir2", 'hello').check() assert dest.join("dir1", "dir2", "hello").check()
def test_rsync_same_popen_twice(self, config, mysetup, hookrecorder, def test_rsync_same_popen_twice(
slavecontroller): self, config, mysetup, hookrecorder, workercontroller
):
source, dest = mysetup.source, mysetup.dest source, dest = mysetup.source, mysetup.dest
hm = NodeManager(config, ["popen//chdir=%s" % dest] * 2) hm = NodeManager(config, ["popen//chdir=%s" % dest] * 2)
hm.roots = [] hm.roots = []
@@ -140,9 +142,9 @@ class TestHRSync:
files = list(source.visit(rec=syncer.filter, fil=syncer.filter)) files = list(source.visit(rec=syncer.filter, fil=syncer.filter))
assert len(files) == 3 assert len(files) == 3
basenames = [x.basename for x in files] basenames = [x.basename for x in files]
assert 'dir' in basenames assert "dir" in basenames
assert 'file.txt' in basenames assert "file.txt" in basenames
assert 'somedir' in basenames assert "somedir" in basenames
def test_hrsync_one_host(self, mysetup): def test_hrsync_one_host(self, mysetup):
source, dest = mysetup.source, mysetup.dest source, dest = mysetup.source, mysetup.dest
@@ -167,41 +169,48 @@ class TestNodeManager:
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())").receive_each() "import os ; channel.send(os.getcwd())"
).receive_each()
p = py.path.local(p) p = py.path.local(p)
py.builtin.print_("remote curdir", p) print("remote curdir", p)
assert p == mysetup.dest.join(config.topdir.basename) assert p == mysetup.dest.join(config.topdir.basename)
assert p.join("dir1").check() assert p.join("dir1").check()
assert p.join("dir1", "file1").check() assert p.join("dir1", "file1").check()
def test_popen_rsync_subdir(self, testdir, mysetup, slavecontroller): def test_popen_rsync_subdir(self, testdir, mysetup, workercontroller):
source, dest = mysetup.source, mysetup.dest source, dest = mysetup.source, mysetup.dest
dir1 = mysetup.source.mkdir("dir1") dir1 = mysetup.source.mkdir("dir1")
dir2 = dir1.mkdir("dir2") dir2 = dir1.mkdir("dir2")
dir2.ensure("hello") dir2.ensure("hello")
for rsyncroot in (dir1, source): for rsyncroot in (dir1, source):
dest.remove() dest.remove()
nodemanager = NodeManager(testdir.parseconfig( nodemanager = NodeManager(
"--tx", "popen//chdir=%s" % dest, "--rsyncdir", rsyncroot, testdir.parseconfig(
source, )) "--tx", "popen//chdir=%s" % dest, "--rsyncdir", rsyncroot, source
)
)
nodemanager.setup_nodes(None) # calls .rsync_roots() nodemanager.setup_nodes(None) # calls .rsync_roots()
if rsyncroot == source: if rsyncroot == source:
dest = dest.join("source") dest = dest.join("source")
assert dest.join("dir1").check() assert dest.join("dir1").check()
assert dest.join("dir1", "dir2").check() assert dest.join("dir1", "dir2").check()
assert dest.join("dir1", "dir2", 'hello').check() assert dest.join("dir1", "dir2", "hello").check()
nodemanager.teardown_nodes() nodemanager.teardown_nodes()
def test_init_rsync_roots(self, testdir, mysetup, slavecontroller): def test_init_rsync_roots(self, testdir, mysetup, workercontroller):
source, dest = mysetup.source, mysetup.dest source, dest = mysetup.source, mysetup.dest
dir2 = source.ensure("dir1", "dir2", dir=1) dir2 = source.ensure("dir1", "dir2", dir=1)
source.ensure("dir1", "somefile", dir=1) source.ensure("dir1", "somefile", dir=1)
dir2.ensure("hello") dir2.ensure("hello")
source.ensure("bogusdir", "file") source.ensure("bogusdir", "file")
source.join("tox.ini").write(py.std.textwrap.dedent(""" source.join("tox.ini").write(
textwrap.dedent(
"""
[pytest] [pytest]
rsyncdirs=dir1/dir2 rsyncdirs=dir1/dir2
""")) """
)
)
config = testdir.parseconfig(source) config = testdir.parseconfig(source)
nodemanager = NodeManager(config, ["popen//chdir=%s" % dest]) nodemanager = NodeManager(config, ["popen//chdir=%s" % dest])
nodemanager.setup_nodes(None) # calls .rsync_roots() nodemanager.setup_nodes(None) # calls .rsync_roots()
@@ -209,7 +218,7 @@ class TestNodeManager:
assert not dest.join("dir1").check() assert not dest.join("dir1").check()
assert not dest.join("bogus").check() assert not dest.join("bogus").check()
def test_rsyncignore(self, testdir, mysetup, slavecontroller): def test_rsyncignore(self, testdir, mysetup, workercontroller):
source, dest = mysetup.source, mysetup.dest source, dest = mysetup.source, mysetup.dest
dir2 = source.ensure("dir1", "dir2", dir=1) dir2 = source.ensure("dir1", "dir2", dir=1)
source.ensure("dir5", "dir6", "bogus") source.ensure("dir5", "dir6", "bogus")
@@ -217,27 +226,31 @@ class TestNodeManager:
dir2.ensure("hello") dir2.ensure("hello")
source.ensure("foo", "bar") source.ensure("foo", "bar")
source.ensure("bar", "foo") source.ensure("bar", "foo")
source.join("tox.ini").write(py.std.textwrap.dedent(""" source.join("tox.ini").write(
textwrap.dedent(
"""
[pytest] [pytest]
rsyncdirs = dir1 dir5 rsyncdirs = dir1 dir5
rsyncignore = dir1/dir2 dir5/dir6 foo* rsyncignore = dir1/dir2 dir5/dir6 foo*
""")) """
)
)
config = testdir.parseconfig(source) config = testdir.parseconfig(source)
config.option.rsyncignore = ['bar'] config.option.rsyncignore = ["bar"]
nodemanager = NodeManager(config, ["popen//chdir=%s" % dest]) nodemanager = NodeManager(config, ["popen//chdir=%s" % dest])
nodemanager.setup_nodes(None) # calls .rsync_roots() nodemanager.setup_nodes(None) # calls .rsync_roots()
assert dest.join("dir1").check() assert dest.join("dir1").check()
assert not dest.join("dir1", "dir2").check() assert not dest.join("dir1", "dir2").check()
assert dest.join("dir5", "file").check() assert dest.join("dir5", "file").check()
assert not dest.join("dir6").check() assert not dest.join("dir6").check()
assert not dest.join('foo').check() assert not dest.join("foo").check()
assert not dest.join('bar').check() assert not dest.join("bar").check()
def test_optimise_popen(self, testdir, mysetup, slavecontroller): def test_optimise_popen(self, testdir, mysetup, workercontroller):
source = mysetup.source source = mysetup.source
specs = ["popen"] * 3 specs = ["popen"] * 3
source.join("conftest.py").write("rsyncdirs = ['a']") source.join("conftest.py").write("rsyncdirs = ['a']")
source.ensure('a', dir=1) source.ensure("a", dir=1)
config = testdir.parseconfig(source) config = testdir.parseconfig(source)
nodemanager = NodeManager(config, specs) nodemanager = NodeManager(config, specs)
nodemanager.setup_nodes(None) # calls .rysnc_roots() nodemanager.setup_nodes(None) # calls .rysnc_roots()
@@ -246,12 +259,15 @@ class TestNodeManager:
assert not gwspec.chdir assert not gwspec.chdir
def test_ssh_setup_nodes(self, specssh, testdir): def test_ssh_setup_nodes(self, specssh, testdir):
testdir.makepyfile(__init__="", testdir.makepyfile(
test_x=""" __init__="",
test_x="""
def test_one(): def test_one():
pass pass
""") """,
reprec = testdir.inline_run("-d", "--rsyncdir=%s" % testdir.tmpdir, )
"--tx", specssh, testdir.tmpdir) reprec = testdir.inline_run(
"-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

42
tox.ini
View File

@@ -1,11 +1,10 @@
[tox] [tox]
# if you change the envlist, please update .travis.yml file as well # if you change the envlist, please update .travis.yml file as well
envlist= envlist=
py{27,34,35,36}-pytest{30,31,32,33} linting
py{27,36}-pytest{30,31,32,33}-pexpect py{27,34,35,36}-pytest{30,31,32,33,36}
py{27,36}-pytest36-pexpect
py{27,36}-pytest{master,features} py{27,36}-pytest{master,features}
flakes
readme
[testenv] [testenv]
@@ -19,6 +18,7 @@ deps =
pytest31: pytest~=3.1.0 pytest31: pytest~=3.1.0
pytest32: pytest~=3.2.0 pytest32: pytest~=3.2.0
pytest33: pytest~=3.3.0 pytest33: pytest~=3.3.0
pytest36: pytest~=3.6.0
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 pytestfeatures: git+https://github.com/pytest-dev/pytest.git@features
pexpect: pexpect pexpect: pexpect
@@ -27,20 +27,30 @@ platform=
commands= commands=
# always clean to avoid code unmarshal mismatch on old python/pytest # always clean to avoid code unmarshal mismatch on old python/pytest
py.cleanup -aq py.cleanup -aq
py.test {posargs} pytest {posargs}
[testenv:flakes] [testenv:linting]
skipsdist = True
usedevelop = True
deps =
pre-commit
commands = pre-commit run --all-files --show-diff-on-failure
[testenv:release]
changedir= changedir=
deps = flake8 decription = do a release, required posarg of the version number
commands = flake8 setup.py testing xdist basepython = python3.6
skipsdist = True
[testenv:readme] usedevelop = True
changedir = passenv = *
deps = readme deps =
skip_install = true towncrier
commands = python setup.py check -r -s commands =
towncrier --version {posargs} --yes
[pytest] [pytest]
addopts = -rsfxX addopts = -rsfxX
;; hello
[flake8]
max-line-length = 120
ignore = E203,W503

View File

@@ -1,3 +1,3 @@
from xdist._version import version as __version__ from xdist._version import version as __version__
__all__ = ['__version__'] __all__ = ["__version__"]

View File

@@ -1,7 +1,7 @@
import py import py
import pytest import pytest
from xdist.slavemanage import NodeManager from xdist.workermanage import NodeManager
from xdist.scheduler import ( from xdist.scheduler import (
EachScheduling, EachScheduling,
LoadScheduling, LoadScheduling,
@@ -10,23 +10,23 @@ from xdist.scheduler import (
) )
queue = py.builtin._tryimport('queue', 'Queue') from six.moves.queue import Empty, Queue
class Interrupted(KeyboardInterrupt): class Interrupted(KeyboardInterrupt):
""" signals an immediate interruption. """ """ signals an immediate interruption. """
class DSession: class DSession(object):
"""A py.test plugin which runs a distributed test session """A pytest plugin which runs a distributed test session
At the beginning of the test session this creates a NodeManager At the beginning of the test session this creates a NodeManager
instance which creates and starts all nodes. Nodes then emit instance which creates and starts all nodes. Nodes then emit
events processed in the pytest_runtestloop hook using the slave_* events processed in the pytest_runtestloop hook using the worker_*
methods. methods.
Once a node is started it will automatically start running the Once a node is started it will automatically start running the
py.test mainloop with some custom hooks. This means a node pytest mainloop with some custom hooks. This means a node
automatically starts collecting tests. Once tests are collected automatically starts collecting tests. Once tests are collected
it will wait for instructions. it will wait for instructions.
""" """
@@ -41,14 +41,14 @@ class DSession:
self.shuttingdown = False self.shuttingdown = False
self.countfailures = 0 self.countfailures = 0
self.maxfail = config.getvalue("maxfail") self.maxfail = config.getvalue("maxfail")
self.queue = queue.Queue() self.queue = Queue()
self._session = None self._session = None
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_slave_restart = self.config.getoption('max_slave_restart') self._max_worker_restart = self.config.option.maxworkerrestart
if self._max_slave_restart is not None: if self._max_worker_restart is not None:
self._max_slave_restart = int(self._max_slave_restart) self._max_worker_restart = int(self._max_worker_restart)
try: try:
self.terminal = config.pluginmanager.getplugin("terminalreporter") self.terminal = config.pluginmanager.getplugin("terminalreporter")
except KeyError: except KeyError:
@@ -75,7 +75,7 @@ class DSession:
"""Creates and starts the nodes. """Creates and starts the nodes.
The nodes are setup to put their events onto self.queue. As The nodes are setup to put their events onto self.queue. As
soon as nodes start they will emit the slave_slaveready event. soon as nodes start they will emit the worker_workerready event.
""" """
self.nodemanager = NodeManager(self.config) self.nodemanager = NodeManager(self.config)
nodes = self.nodemanager.setup_nodes(putevent=self.queue.put) nodes = self.nodemanager.setup_nodes(putevent=self.queue.put)
@@ -84,7 +84,7 @@ class DSession:
def pytest_sessionfinish(self, session): def pytest_sessionfinish(self, session):
"""Shutdown all nodes.""" """Shutdown all nodes."""
nm = getattr(self, 'nodemanager', None) # if not fully initialized nm = getattr(self, "nodemanager", None) # if not fully initialized
if nm is not None: if nm is not None:
nm.teardown_nodes() nm.teardown_nodes()
self._session = None self._session = None
@@ -95,19 +95,18 @@ class DSession:
@pytest.mark.trylast @pytest.mark.trylast
def pytest_xdist_make_scheduler(self, config, log): def pytest_xdist_make_scheduler(self, config, log):
dist = config.getvalue('dist') dist = config.getvalue("dist")
schedulers = { schedulers = {
'each': EachScheduling, "each": EachScheduling,
'load': LoadScheduling, "load": LoadScheduling,
'loadscope': LoadScopeScheduling, "loadscope": LoadScopeScheduling,
'loadfile': LoadFileScheduling, "loadfile": LoadFileScheduling,
} }
return schedulers[dist](config, log) return schedulers[dist](config, log)
def pytest_runtestloop(self): def pytest_runtestloop(self):
self.sched = self.config.hook.pytest_xdist_make_scheduler( self.sched = self.config.hook.pytest_xdist_make_scheduler(
config=self.config, config=self.config, log=self.log
log=self.log
) )
assert self.sched is not None assert self.sched is not None
@@ -120,7 +119,7 @@ class DSession:
return True return True
def loop_once(self): def loop_once(self):
"""Process one callback from one of the slaves.""" """Process one callback from one of the workers."""
while 1: while 1:
if not self._active_nodes: if not self._active_nodes:
# If everything has died stop looping # If everything has died stop looping
@@ -129,11 +128,11 @@ class DSession:
try: try:
eventcall = self.queue.get(timeout=2.0) eventcall = self.queue.get(timeout=2.0)
break break
except queue.Empty: except Empty:
continue continue
callname, kwargs = eventcall callname, kwargs = eventcall
assert callname, kwargs assert callname, kwargs
method = "slave_" + callname method = "worker_" + callname
call = getattr(self, method) call = getattr(self, method)
self.log("calling method", method, kwargs) self.log("calling method", method, kwargs)
call(**kwargs) call(**kwargs)
@@ -141,44 +140,48 @@ class DSession:
self.triggershutdown() self.triggershutdown()
# #
# callbacks for processing events from slaves # callbacks for processing events from workers
# #
def slave_slaveready(self, node, slaveinfo): def worker_workerready(self, node, workerinfo):
"""Emitted when a node first starts up. """Emitted when a node first starts up.
This adds the node to the scheduler, nodes continue with This adds the node to the scheduler, nodes continue with
collection without any further input. collection without any further input.
""" """
node.slaveinfo = slaveinfo node.workerinfo = workerinfo
node.slaveinfo['id'] = node.gateway.id node.workerinfo["id"] = node.gateway.id
node.slaveinfo['spec'] = node.gateway.spec node.workerinfo["spec"] = node.gateway.spec
# TODO: (#234 task) needs this for pytest. Remove when refactor in pytest repo
node.slaveinfo = node.workerinfo
self.config.hook.pytest_testnodeready(node=node) self.config.hook.pytest_testnodeready(node=node)
if self.shuttingdown: if self.shuttingdown:
node.shutdown() node.shutdown()
else: else:
self.sched.add_node(node) self.sched.add_node(node)
def slave_slavefinished(self, node): def worker_workerfinished(self, node):
"""Emitted when node executes its pytest_sessionfinish hook. """Emitted when node executes its pytest_sessionfinish hook.
Removes the node from the scheduler. Removes the node from the scheduler.
The node might not be in the scheduler if it had not emitted The node might not be in the scheduler if it had not emitted
slaveready before shutdown was triggered. workerready before shutdown was triggered.
""" """
self.config.hook.pytest_testnodedown(node=node, error=None) self.config.hook.pytest_testnodedown(node=node, error=None)
if node.slaveoutput['exitstatus'] == 2: # keyboard-interrupt if node.workeroutput["exitstatus"] == 2: # keyboard-interrupt
self.shouldstop = "%s received keyboard-interrupt" % (node,) self.shouldstop = "%s received keyboard-interrupt" % (node,)
self.slave_errordown(node, "keyboard-interrupt") self.worker_errordown(node, "keyboard-interrupt")
return return
if node in self.sched.nodes: if node in self.sched.nodes:
crashitem = self.sched.remove_node(node) crashitem = self.sched.remove_node(node)
assert not crashitem, (crashitem, node) assert not crashitem, (crashitem, node)
self._active_nodes.remove(node) self._active_nodes.remove(node)
def slave_errordown(self, node, error): def worker_errordown(self, node, error):
"""Emitted by the SlaveController when a node dies.""" """Emitted by the WorkerController when a node dies."""
self.config.hook.pytest_testnodedown(node=node, error=error) self.config.hook.pytest_testnodedown(node=node, error=error)
try: try:
crashitem = self.sched.remove_node(node) crashitem = self.sched.remove_node(node)
@@ -189,22 +192,23 @@ class DSession:
self.handle_crashitem(crashitem, node) self.handle_crashitem(crashitem, node)
self._failed_nodes_count += 1 self._failed_nodes_count += 1
maximum_reached = (self._max_slave_restart is not None and maximum_reached = (
self._failed_nodes_count > self._max_slave_restart) self._max_worker_restart is not None
and self._failed_nodes_count > self._max_worker_restart
)
if maximum_reached: if maximum_reached:
if self._max_slave_restart == 0: if self._max_worker_restart == 0:
msg = 'Slave restarting disabled' msg = "Worker restarting disabled"
else: else:
msg = "Maximum crashed slaves reached: %d" % \ msg = "Maximum crashed workers reached: %d" % self._max_worker_restart
self._max_slave_restart
self.report_line(msg) self.report_line(msg)
else: else:
self.report_line("Replacing crashed slave %s" % node.gateway.id) self.report_line("Replacing 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 slave_collectionfinish(self, node, ids): def worker_collectionfinish(self, node, ids):
"""Slave has finished test collection. """worker has finished test collection.
This adds the collection for this node to the scheduler. If This adds the collection for this node to the scheduler. If
the scheduler indicates collection is finished (i.e. all the scheduler indicates collection is finished (i.e. all
@@ -214,8 +218,7 @@ class DSession:
""" """
if self.shuttingdown: if self.shuttingdown:
return return
self.config.hook.pytest_xdist_node_collection_finished(node=node, self.config.hook.pytest_xdist_node_collection_finished(node=node, ids=ids)
ids=ids)
# tell session which items were effectively collected otherwise # tell session which items were effectively collected otherwise
# the master node will finish the session with EXIT_NOTESTSCOLLECTED # the master node will finish the session with EXIT_NOTESTSCOLLECTED
self._session.testscollected = len(ids) self._session.testscollected = len(ids)
@@ -226,27 +229,26 @@ class DSession:
if self.terminal and not self.sched.has_pending: if self.terminal and not self.sched.has_pending:
self.trdist.ensure_show_status() self.trdist.ensure_show_status()
self.terminal.write_line("") self.terminal.write_line("")
self.terminal.write_line("scheduling tests via %s" % ( self.terminal.write_line(
self.sched.__class__.__name__)) "scheduling tests via %s" % (self.sched.__class__.__name__)
)
self.sched.schedule() self.sched.schedule()
def slave_logstart(self, node, nodeid, location): def worker_logstart(self, node, nodeid, location):
"""Emitted when a node calls the pytest_runtest_logstart hook.""" """Emitted when a node calls the pytest_runtest_logstart hook."""
self.config.hook.pytest_runtest_logstart( self.config.hook.pytest_runtest_logstart(nodeid=nodeid, location=location)
nodeid=nodeid, location=location)
def slave_logfinish(self, node, nodeid, location): def worker_logfinish(self, node, nodeid, location):
"""Emitted when a node calls the pytest_runtest_logfinish hook.""" """Emitted when a node calls the pytest_runtest_logfinish hook."""
self.config.hook.pytest_runtest_logfinish( self.config.hook.pytest_runtest_logfinish(nodeid=nodeid, location=location)
nodeid=nodeid, location=location)
def slave_testreport(self, node, rep): def worker_testreport(self, node, rep):
"""Emitted when a node calls the pytest_runtest_logreport hook.""" """Emitted when a node calls the pytest_runtest_logreport hook."""
rep.node = node rep.node = node
self.config.hook.pytest_runtest_logreport(report=rep) self.config.hook.pytest_runtest_logreport(report=rep)
self._handlefailures(rep) self._handlefailures(rep)
def slave_runtest_protocol_complete(self, node, item_index, duration): def worker_runtest_protocol_complete(self, node, item_index, duration):
""" """
Emitted when a node fires the 'runtest_protocol_complete' event, Emitted when a node fires the 'runtest_protocol_complete' event,
signalling that a test has completed the runtestprotocol and should be signalling that a test has completed the runtestprotocol and should be
@@ -254,12 +256,16 @@ class DSession:
""" """
self.sched.mark_test_complete(node, item_index, duration) self.sched.mark_test_complete(node, item_index, duration)
def slave_collectreport(self, node, rep): def worker_collectreport(self, node, rep):
"""Emitted when a node calls the pytest_collectreport hook.""" """Emitted when a node calls the pytest_collectreport hook.
if rep.failed:
self._failed_slave_collectreport(node, rep)
def slave_logwarning(self, message, code, nodeid, fslocation): Because we only need the report when there's a failure, as optimization
we only expect to receive failed reports from workers (#330).
"""
assert rep.failed
self._failed_worker_collectreport(node, rep)
def worker_logwarning(self, message, code, nodeid, fslocation):
"""Emitted when a node calls the pytest_logwarning hook.""" """Emitted when a node calls the pytest_logwarning hook."""
kwargs = dict(message=message, code=code, nodeid=nodeid, fslocation=fslocation) kwargs = dict(message=message, code=code, nodeid=nodeid, fslocation=fslocation)
self.config.hook.pytest_logwarning.call_historic(kwargs=kwargs) self.config.hook.pytest_logwarning.call_historic(kwargs=kwargs)
@@ -270,7 +276,7 @@ class DSession:
This is normally for when a node dies, this will copy the spec This is normally for when a node dies, this will copy the spec
of the existing node and create a new one with a new id. The of the existing node and create a new one with a new id. The
new node will have been setup so it will start calling the new node will have been setup so it will start calling the
"slave_*" hooks and do work soon. "worker_*" hooks and do work soon.
""" """
spec = node.gateway.spec spec = node.gateway.spec
spec.id = None spec.id = None
@@ -279,9 +285,9 @@ class DSession:
self._active_nodes.add(node) self._active_nodes.add(node)
return node return node
def _failed_slave_collectreport(self, node, rep): def _failed_worker_collectreport(self, node, rep):
# Check we haven't already seen this report (from # Check we haven't already seen this report (from
# another slave). # another worker).
if rep.longrepr not in self._failed_collection_errors: if rep.longrepr not in self._failed_collection_errors:
self._failed_collection_errors[rep.longrepr] = True self._failed_collection_errors[rep.longrepr] = True
self.config.hook.pytest_collectreport(report=rep) self.config.hook.pytest_collectreport(report=rep)
@@ -291,8 +297,7 @@ class DSession:
if rep.failed: if rep.failed:
self.countfailures += 1 self.countfailures += 1
if self.maxfail and self.countfailures >= self.maxfail: if self.maxfail and self.countfailures >= self.maxfail:
self.shouldstop = "stopping after %d failures" % ( self.shouldstop = "stopping after %d failures" % (self.countfailures)
self.countfailures)
def triggershutdown(self): def triggershutdown(self):
self.log("triggering shutdown") self.log("triggering shutdown")
@@ -300,25 +305,26 @@ class DSession:
for node in self.sched.nodes: for node in self.sched.nodes:
node.shutdown() node.shutdown()
def handle_crashitem(self, nodeid, slave): def handle_crashitem(self, nodeid, worker):
# XXX get more reporting info by recording pytest_runtest_logstart? # XXX get more reporting info by recording pytest_runtest_logstart?
# 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 = "Slave %r crashed while running %r" % (slave.gateway.id, nodeid) msg = "Worker %r crashed while running %r" % (worker.gateway.id, nodeid)
rep = runner.TestReport(nodeid, (fspath, None, fspath), rep = runner.TestReport(
(), "failed", msg, "???") nodeid, (fspath, None, fspath), (), "failed", msg, "???"
rep.node = slave )
rep.node = worker
self.config.hook.pytest_runtest_logreport(report=rep) self.config.hook.pytest_runtest_logreport(report=rep)
class TerminalDistReporter: class TerminalDistReporter(object):
def __init__(self, config): def __init__(self, config):
self.config = config self.config = config
self.tr = config.pluginmanager.getplugin("terminalreporter") self.tr = config.pluginmanager.getplugin("terminalreporter")
self._status = {} self._status = {}
self._lastlen = 0 self._lastlen = 0
self._isatty = getattr(self.tr, 'isatty', self.tr.hasmarkup) self._isatty = getattr(self.tr, "isatty", self.tr.hasmarkup)
def write_line(self, msg): def write_line(self, msg):
self.tr.write_line(msg) self.tr.write_line(msg)
@@ -333,8 +339,7 @@ class TerminalDistReporter:
self.rewrite(self.getstatus()) self.rewrite(self.getstatus())
def getstatus(self): def getstatus(self):
parts = ["%s %s" % (spec.id, self._status[spec.id]) parts = ["%s %s" % (spec.id, self._status[spec.id]) for spec in self._specs]
for spec in self._specs]
return " / ".join(parts) return " / ".join(parts)
def rewrite(self, line, newline=False): def rewrite(self, line, newline=False):
@@ -357,17 +362,17 @@ class TerminalDistReporter:
if self.config.option.verbose > 0: if self.config.option.verbose > 0:
rinfo = gateway._rinfo() rinfo = gateway._rinfo()
version = "%s.%s.%s" % rinfo.version_info[:3] version = "%s.%s.%s" % rinfo.version_info[:3]
self.rewrite("[%s] %s Python %s cwd: %s" % ( self.rewrite(
gateway.id, rinfo.platform, version, rinfo.cwd), "[%s] %s Python %s cwd: %s"
newline=True) % (gateway.id, rinfo.platform, version, rinfo.cwd),
newline=True,
)
self.setstatus(gateway.spec, "C") self.setstatus(gateway.spec, "C")
def pytest_testnodeready(self, node): def pytest_testnodeready(self, node):
if self.config.option.verbose > 0: if self.config.option.verbose > 0:
d = node.slaveinfo d = node.workerinfo
infoline = "[%s] Python %s" % ( infoline = "[%s] Python %s" % (d["id"], d["version"].replace("\n", " -- "))
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")

View File

@@ -1,34 +1,38 @@
""" """
Implement -f aka looponfailing for py.test. Implement -f aka looponfailing for pytest.
NOTE that we try to avoid loading and depending on application modules NOTE that we try to avoid loading and depending on application modules
within the controlling process (the one that starts repeatedly test within the controlling process (the one that starts repeatedly test
processes) otherwise changes to source code can crash processes) otherwise changes to source code can crash
the controlling process which should best never happen. the controlling process which should best never happen.
""" """
from __future__ import print_function
import py import py
import pytest import pytest
import sys import sys
import time
import execnet import execnet
def pytest_addoption(parser): def pytest_addoption(parser):
group = parser.getgroup("xdist", "distributed and subprocess testing") group = parser.getgroup("xdist", "distributed and subprocess testing")
group._addoption( group._addoption(
'-f', '--looponfail', "-f",
action="store_true", dest="looponfail", default=False, "--looponfail",
action="store_true",
dest="looponfail",
default=False,
help="run tests in subprocess, wait for modified files " help="run tests in subprocess, wait for modified files "
"and re-run failing test set until all pass.") "and re-run failing test set until all pass.",
)
def pytest_cmdline_main(config): def pytest_cmdline_main(config):
if config.getoption("looponfail"): if config.getoption("looponfail"):
usepdb = config.getoption('usepdb') # a core option usepdb = config.getoption("usepdb") # a core option
if usepdb: if usepdb:
raise pytest.UsageError( raise pytest.UsageError("--pdb incompatible with --looponfail.")
"--pdb incompatible with --looponfail.")
looponfail_main(config) looponfail_main(config)
return 2 # looponfail only can get stop with ctrl-C anyway return 2 # looponfail only can get stop with ctrl-C anyway
@@ -44,8 +48,8 @@ def looponfail_main(config):
# the last failures passed, let's immediately rerun all # the last failures passed, let's immediately rerun all
continue continue
repr_pytest_looponfailinfo( repr_pytest_looponfailinfo(
failreports=remotecontrol.failures, failreports=remotecontrol.failures, rootdirs=rootdirs
rootdirs=rootdirs) )
statrecorder.waitonchange(checkinterval=2.0) statrecorder.waitonchange(checkinterval=2.0)
except KeyboardInterrupt: except KeyboardInterrupt:
print() print()
@@ -59,7 +63,7 @@ class RemoteControl(object):
def trace(self, *args): def trace(self, *args):
if self.config.option.debug: if self.config.option.debug:
msg = " ".join([str(x) for x in args]) msg = " ".join([str(x) for x in args])
py.builtin.print_("RemoteControl:", msg) print("RemoteControl:", msg)
def initgateway(self): def initgateway(self):
return execnet.makegateway("popen") return execnet.makegateway("popen")
@@ -67,12 +71,12 @@ class RemoteControl(object):
def setup(self, out=None): def setup(self, out=None):
if out is None: if out is None:
out = py.io.TerminalWriter() out = py.io.TerminalWriter()
if hasattr(self, 'gateway'): if hasattr(self, "gateway"):
raise ValueError("already have gateway %r" % self.gateway) raise ValueError("already have gateway %r" % self.gateway)
self.trace("setting up slave session") self.trace("setting up worker session")
self.gateway = self.initgateway() self.gateway = self.initgateway()
self.channel = channel = self.gateway.remote_exec( self.channel = channel = self.gateway.remote_exec(
init_slave_session, init_worker_session,
args=self.config.args, args=self.config.args,
option_dict=vars(self.config.option), option_dict=vars(self.config.option),
) )
@@ -81,15 +85,16 @@ class RemoteControl(object):
def write(s): def write(s):
out._file.write(s) out._file.write(s)
out._file.flush() out._file.flush()
remote_outchannel.setcallback(write) remote_outchannel.setcallback(write)
def ensure_teardown(self): def ensure_teardown(self):
if hasattr(self, 'channel'): if hasattr(self, "channel"):
if not self.channel.isclosed(): if not self.channel.isclosed():
self.trace("closing", self.channel) self.trace("closing", self.channel)
self.channel.close() self.channel.close()
del self.channel del self.channel
if hasattr(self, 'gateway'): if hasattr(self, "gateway"):
self.trace("exiting", self.gateway) self.trace("exiting", self.gateway)
self.gateway.exit() self.gateway.exit()
del self.gateway del self.gateway
@@ -134,11 +139,12 @@ def repr_pytest_looponfailinfo(failreports, rootdirs):
tr.line("### Watching: %s" % (rootdir,), bold=True) tr.line("### Watching: %s" % (rootdir,), bold=True)
def init_slave_session(channel, args, option_dict): def init_worker_session(channel, args, option_dict):
import os import os
import sys import sys
outchannel = channel.gateway.newchannel() outchannel = channel.gateway.newchannel()
sys.stdout = sys.stderr = outchannel.makefile('w') sys.stdout = sys.stderr = outchannel.makefile("w")
channel.send(outchannel) channel.send(outchannel)
# prune sys.path to not contain relative paths # prune sys.path to not contain relative paths
newpaths = [] newpaths = []
@@ -151,13 +157,15 @@ def init_slave_session(channel, args, option_dict):
# fullwidth, hasmarkup = channel.receive() # fullwidth, hasmarkup = channel.receive()
from _pytest.config import Config from _pytest.config import Config
config = Config.fromdictargs(option_dict, list(args)) config = Config.fromdictargs(option_dict, list(args))
config.args = args config.args = args
from xdist.looponfail import SlaveFailSession from xdist.looponfail import WorkerFailSession
SlaveFailSession(config, channel).main()
WorkerFailSession(config, channel).main()
class SlaveFailSession: class WorkerFailSession(object):
def __init__(self, config, channel): def __init__(self, config, channel):
self.config = config self.config = config
self.channel = channel self.channel = channel
@@ -180,7 +188,8 @@ class SlaveFailSession:
except pytest.UsageError: except pytest.UsageError:
items = session.perform_collect(None) items = session.perform_collect(None)
hook.pytest_collection_modifyitems( hook.pytest_collection_modifyitems(
session=session, config=session.config, items=items) session=session, config=session.config, items=items
)
hook.pytest_collection_finish(session=session) hook.pytest_collection_finish(session=session)
return True return True
@@ -194,11 +203,11 @@ class SlaveFailSession:
self.collection_failed = True self.collection_failed = True
def main(self): def main(self):
self.DEBUG("SLAVE: received configuration, waiting for command trails") self.DEBUG("WORKER: received configuration, waiting for command trails")
try: try:
command = self.channel.receive() command = self.channel.receive()
except KeyboardInterrupt: except KeyboardInterrupt:
return # in the slave we can't do much about this return # in the worker we can't do much about this
self.DEBUG("received", command) self.DEBUG("received", command)
self.current_command = command self.current_command = command
self.config.hook.pytest_cmdline_main(config=self.config) self.config.hook.pytest_cmdline_main(config=self.config)
@@ -206,12 +215,12 @@ class SlaveFailSession:
for rep in self.recorded_failures: for rep in self.recorded_failures:
trails.append(rep.nodeid) trails.append(rep.nodeid)
loc = rep.longrepr loc = rep.longrepr
loc = str(getattr(loc, 'reprcrash', loc)) loc = str(getattr(loc, "reprcrash", loc))
failreports.append(loc) failreports.append(loc)
self.channel.send((trails, failreports, self.collection_failed)) self.channel.send((trails, failreports, self.collection_failed))
class StatRecorder: class StatRecorder(object):
def __init__(self, rootdirlist): def __init__(self, rootdirlist):
self.rootdirlist = rootdirlist self.rootdirlist = rootdirlist
self.statcache = {} self.statcache = {}
@@ -228,7 +237,7 @@ class StatRecorder:
changed = self.check() changed = self.check()
if changed: if changed:
return return
py.std.time.sleep(checkinterval) time.sleep(checkinterval)
def check(self, removepycfiles=True): # noqa, too complex def check(self, removepycfiles=True): # noqa, too complex
changed = False changed = False
@@ -244,10 +253,12 @@ class StatRecorder:
changed = True changed = True
else: else:
if oldstat: if oldstat:
if oldstat.mtime != curstat.mtime or \ if (
oldstat.size != curstat.size: oldstat.mtime != curstat.mtime
or oldstat.size != curstat.size
):
changed = True changed = True
py.builtin.print_("# MODIFIED", path) print("# MODIFIED", path)
if removepycfiles and path.ext == ".py": if removepycfiles and path.ext == ".py":
pycfile = path + "c" pycfile = path + "c"
if pycfile.check(): if pycfile.check():

View File

@@ -1,18 +1,35 @@
import os
import py import py
import pytest import pytest
def parse_numprocesses(s): def auto_detect_cpus():
if s == 'auto': try:
from os import sched_getaffinity
except ImportError:
if os.environ.get("TRAVIS") == "true":
# workaround https://bitbucket.org/pypy/pypy/issues/2375
return 2
try: try:
from os import cpu_count from os import cpu_count
except ImportError: except ImportError:
from multiprocessing import cpu_count from multiprocessing import cpu_count
try: else:
n = cpu_count()
except NotImplementedError: def cpu_count():
return 1 return len(sched_getaffinity(0))
return n if n else 1
try:
n = cpu_count()
except NotImplementedError:
return 1
return n if n else 1
def parse_numprocesses(s):
if s == "auto":
return auto_detect_cpus()
else: else:
return int(s) return int(s)
@@ -20,57 +37,101 @@ def parse_numprocesses(s):
def pytest_addoption(parser): def pytest_addoption(parser):
group = parser.getgroup("xdist", "distributed and subprocess testing") group = parser.getgroup("xdist", "distributed and subprocess testing")
group._addoption( group._addoption(
'-n', '--numprocesses', dest="numprocesses", metavar="numprocesses", "-n",
"--numprocesses",
dest="numprocesses",
metavar="numprocesses",
action="store", action="store",
type=parse_numprocesses, type=parse_numprocesses,
help="shortcut for '--dist=load --tx=NUM*popen', " help="shortcut for '--dist=load --tx=NUM*popen', "
"you can use 'auto' here for auto detection CPUs number on " "you can use 'auto' here for auto detection CPUs number on "
"host system") "host system",
group.addoption('--max-slave-restart', action="store", default=None, )
help="maximum number of slaves that can be restarted "
"when crashed (set to zero to disable this feature)")
group.addoption( group.addoption(
'--dist', metavar="distmode", "--max-worker-restart",
action="store", choices=['each', 'load', 'loadscope', 'loadfile', 'no'], "--max-slave-restart",
dest="dist", default="no", action="store",
help=("set mode for distributing tests to exec environments.\n\n" default=None,
"each: send each test to all available environments.\n\n" dest="maxworkerrestart",
"load: load balance by sending any pending test to any" help="maximum number of workers that can be restarted "
" available environment.\n\n" "when crashed (set to zero to disable this feature)\n"
"loadscope: load balance by sending pending groups of tests in" "'--max-slave-restart' option is deprecated and will be removed in "
" the same scope to any available environment.\n\n" "a future release",
"loadfile: load balance by sending test grouped by file" )
" to any available environment.\n\n"
"(default) no: run tests inprocess, don't distribute."))
group.addoption( group.addoption(
'--tx', dest="tx", action="append", default=[], "--dist",
metavar="distmode",
action="store",
choices=["each", "load", "loadscope", "loadfile", "no"],
dest="dist",
default="no",
help=(
"set mode for distributing tests to exec environments.\n\n"
"each: send each test to all available environments.\n\n"
"load: load balance by sending any pending test to any"
" available environment.\n\n"
"loadscope: load balance by sending pending groups of tests in"
" the same scope to any available environment.\n\n"
"loadfile: load balance by sending test grouped by file"
" to any available environment.\n\n"
"(default) no: run tests inprocess, don't distribute."
),
)
group.addoption(
"--tx",
dest="tx",
action="append",
default=[],
metavar="xspec", metavar="xspec",
help=("add a test execution environment. some examples: " help=(
"--tx popen//python=python2.5 --tx socket=192.168.1.102:8888 " "add a test execution environment. some examples: "
"--tx ssh=user@codespeak.net//chdir=testcache")) "--tx popen//python=python2.5 --tx socket=192.168.1.102:8888 "
"--tx ssh=user@codespeak.net//chdir=testcache"
),
)
group._addoption( group._addoption(
'-d', "-d",
action="store_true", dest="distload", default=False, action="store_true",
help="load-balance tests. shortcut for '--dist=load'") dest="distload",
default=False,
help="load-balance tests. shortcut for '--dist=load'",
)
group.addoption( group.addoption(
'--rsyncdir', action="append", default=[], metavar="DIR", "--rsyncdir",
help="add directory for rsyncing to remote tx nodes.") action="append",
default=[],
metavar="DIR",
help="add directory for rsyncing to remote tx nodes.",
)
group.addoption( group.addoption(
'--rsyncignore', action="append", default=[], metavar="GLOB", "--rsyncignore",
help="add expression for ignores when rsyncing to remote tx nodes.") action="append",
default=[],
metavar="GLOB",
help="add expression for ignores when rsyncing to remote tx nodes.",
)
group.addoption( group.addoption(
"--boxed", action="store_true", "--boxed",
help="backward compatibility alias for pytest-forked --forked") action="store_true",
help="backward compatibility alias for pytest-forked --forked",
)
parser.addini( parser.addini(
'rsyncdirs', 'list of (relative) paths to be rsynced for' "rsyncdirs",
' remote distributed testing.', type="pathlist") "list of (relative) paths to be rsynced for" " remote distributed testing.",
type="pathlist",
)
parser.addini( parser.addini(
'rsyncignore', 'list of (relative) glob-style paths to be ignored ' "rsyncignore",
'for rsyncing.', type="pathlist") "list of (relative) glob-style paths to be ignored " "for rsyncing.",
type="pathlist",
)
parser.addini( parser.addini(
"looponfailroots", type="pathlist", "looponfailroots",
help="directories to check for changes", default=[py.path.local()]) type="pathlist",
help="directories to check for changes",
default=[py.path.local()],
)
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
@@ -80,12 +141,14 @@ 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 # avoid warnings with pytest-2.8
method = getattr(pluginmanager, "add_hookspecs", None) method = getattr(pluginmanager, "add_hookspecs", None)
if method is None: if method is None:
method = pluginmanager.addhooks method = pluginmanager.addhooks
method(newhooks) method(newhooks)
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
# distributed testing initialization # distributed testing initialization
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
@@ -95,6 +158,7 @@ def pytest_addhooks(pluginmanager):
def pytest_configure(config): def pytest_configure(config):
if config.getoption("dist") != "no" and not config.getvalue("collectonly"): if config.getoption("dist") != "no" and not config.getvalue("collectonly"):
from xdist.dsession import DSession from xdist.dsession import DSession
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")
@@ -106,18 +170,20 @@ def pytest_configure(config):
@pytest.mark.tryfirst @pytest.mark.tryfirst
def pytest_cmdline_main(config): def pytest_cmdline_main(config):
if config.option.numprocesses: if config.option.numprocesses:
if config.option.dist == 'no': if config.option.dist == "no":
config.option.dist = "load" config.option.dist = "load"
config.option.tx = ['popen'] * config.option.numprocesses config.option.tx = ["popen"] * config.option.numprocesses
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"):
usepdb = config.getoption('usepdb') # a core option usepdb = config.getoption("usepdb") # a core option
if val("dist") != "no": if val("dist") != "no":
if usepdb: if usepdb:
raise pytest.UsageError( raise pytest.UsageError(
"--pdb is incompatible with distributing tests; try using -n0.") # noqa: E501 "--pdb is incompatible with distributing tests; try using -n0."
) # noqa: E501
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
# fixtures # fixtures
@@ -129,7 +195,7 @@ def worker_id(request):
"""Return the id of the current worker ('gw0', 'gw1', etc) or 'master' """Return the id of the current worker ('gw0', 'gw1', etc) or 'master'
if running on the master node. if running on the master node.
""" """
if hasattr(request.config, 'slaveinput'): if hasattr(request.config, "workerinput"):
return request.config.slaveinput['slaveid'] return request.config.workerinput["workerid"]
else: else:
return 'master' return "master"

View File

@@ -14,11 +14,11 @@ import _pytest.hookspec
import pytest import pytest
class SlaveInteractor: class WorkerInteractor(object):
def __init__(self, config, channel): def __init__(self, config, channel):
self.config = config self.config = config
self.slaveid = config.slaveinput.get('slaveid', "?") self.workerid = config.workerinput.get("workerid", "?")
self.log = py.log.Producer("slave-%s" % self.slaveid) 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)
self.channel = channel self.channel = channel
@@ -34,14 +34,14 @@ class SlaveInteractor:
def pytest_sessionstart(self, session): def pytest_sessionstart(self, session):
self.session = session self.session = session
slaveinfo = getinfodict() workerinfo = getinfodict()
self.sendevent("slaveready", slaveinfo=slaveinfo) self.sendevent("workerready", workerinfo=workerinfo)
@pytest.hookimpl(hookwrapper=True) @pytest.hookimpl(hookwrapper=True)
def pytest_sessionfinish(self, exitstatus): def pytest_sessionfinish(self, exitstatus):
self.config.slaveoutput['exitstatus'] = exitstatus self.config.workeroutput["exitstatus"] = exitstatus
yield yield
self.sendevent("slavefinished", slaveoutput=self.config.slaveoutput) self.sendevent("workerfinished", workeroutput=self.config.workeroutput)
def pytest_collection(self, session): def pytest_collection(self, session):
self.sendevent("collectionstart") self.sendevent("collectionstart")
@@ -56,7 +56,7 @@ class SlaveInteractor:
return True return True
self.log("received command", name, kwargs) self.log("received command", name, kwargs)
if name == "runtests": if name == "runtests":
torun.extend(kwargs['indices']) torun.extend(kwargs["indices"])
elif name == "runtests_all": elif name == "runtests_all":
torun.extend(range(len(session.items))) torun.extend(range(len(session.items)))
self.log("items to run:", torun) self.log("items to run:", torun)
@@ -79,41 +79,49 @@ class SlaveInteractor:
nextitem = None nextitem = None
start = time.time() start = time.time()
self.config.hook.pytest_runtest_protocol( self.config.hook.pytest_runtest_protocol(item=item, nextitem=nextitem)
item=item,
nextitem=nextitem)
duration = time.time() - start duration = time.time() - start
self.sendevent("runtest_protocol_complete", item_index=self.item_index, self.sendevent(
duration=duration) "runtest_protocol_complete", item_index=self.item_index, duration=duration
)
def pytest_collection_finish(self, session): def pytest_collection_finish(self, session):
self.sendevent( self.sendevent(
"collectionfinish", "collectionfinish",
topdir=str(session.fspath), topdir=str(session.fspath),
ids=[item.nodeid for item in session.items]) ids=[item.nodeid for item in session.items],
)
def pytest_runtest_logstart(self, nodeid, location): def pytest_runtest_logstart(self, nodeid, location):
self.sendevent("logstart", nodeid=nodeid, location=location) self.sendevent("logstart", nodeid=nodeid, location=location)
# the pytest_runtest_logfinish hook was introduced in pytest 3.4 # the pytest_runtest_logfinish hook was introduced in pytest 3.4
if hasattr(_pytest.hookspec, 'pytest_runtest_logfinish'): if hasattr(_pytest.hookspec, "pytest_runtest_logfinish"):
def pytest_runtest_logfinish(self, nodeid, location): def pytest_runtest_logfinish(self, nodeid, location):
self.sendevent("logfinish", nodeid=nodeid, location=location) self.sendevent("logfinish", nodeid=nodeid, location=location)
def pytest_runtest_logreport(self, report): def pytest_runtest_logreport(self, report):
data = serialize_report(report) data = serialize_report(report)
data["item_index"] = self.item_index data["item_index"] = self.item_index
data["worker_id"] = self.slaveid data["worker_id"] = self.workerid
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)
def pytest_collectreport(self, report): def pytest_collectreport(self, report):
data = serialize_report(report) # master only needs reports that failed, as optimization send only them instead (#330)
self.sendevent("collectreport", data=data) if report.failed:
data = serialize_report(report)
self.sendevent("collectreport", data=data)
def pytest_logwarning(self, message, code, nodeid, fslocation): def pytest_logwarning(self, message, code, nodeid, fslocation):
self.sendevent("logwarning", message=message, code=code, nodeid=nodeid, self.sendevent(
fslocation=str(fslocation)) "logwarning",
message=message,
code=code,
nodeid=nodeid,
fslocation=str(fslocation),
)
def serialize_report(rep): def serialize_report(rep):
@@ -122,34 +130,33 @@ def serialize_report(rep):
reprcrash = rep.longrepr.reprcrash.__dict__.copy() reprcrash = rep.longrepr.reprcrash.__dict__.copy()
new_entries = [] new_entries = []
for entry in reprtraceback['reprentries']: for entry in reprtraceback["reprentries"]:
entry_data = { entry_data = {"type": type(entry).__name__, "data": entry.__dict__.copy()}
'type': type(entry).__name__, for key, value in entry_data["data"].items():
'data': entry.__dict__.copy(), if hasattr(value, "__dict__"):
} entry_data["data"][key] = value.__dict__.copy()
for key, value in entry_data['data'].items():
if hasattr(value, '__dict__'):
entry_data['data'][key] = value.__dict__.copy()
new_entries.append(entry_data) new_entries.append(entry_data)
reprtraceback['reprentries'] = new_entries reprtraceback["reprentries"] = new_entries
return { return {
'reprcrash': reprcrash, "reprcrash": reprcrash,
'reprtraceback': reprtraceback, "reprtraceback": reprtraceback,
'sections': rep.longrepr.sections "sections": rep.longrepr.sections,
} }
import py import py
d = rep.__dict__.copy() d = rep.__dict__.copy()
if hasattr(rep.longrepr, 'toterminal'): if hasattr(rep.longrepr, "toterminal"):
if hasattr(rep.longrepr, 'reprtraceback') \ if hasattr(rep.longrepr, "reprtraceback") and hasattr(
and hasattr(rep.longrepr, 'reprcrash'): rep.longrepr, "reprcrash"
d['longrepr'] = disassembled_report(rep) ):
d["longrepr"] = disassembled_report(rep)
else: else:
d['longrepr'] = str(rep.longrepr) d["longrepr"] = str(rep.longrepr)
else: else:
d['longrepr'] = rep.longrepr d["longrepr"] = rep.longrepr
for name in d: for name in d:
if isinstance(d[name], py.path.local): if isinstance(d[name], py.path.local):
d[name] = str(d[name]) d[name] = str(d[name])
@@ -160,6 +167,7 @@ def serialize_report(rep):
def getinfodict(): def getinfodict():
import platform import platform
return dict( return dict(
version=sys.version, version=sys.version,
version_info=tuple(sys.version_info), version_info=tuple(sys.version_info),
@@ -172,7 +180,8 @@ def getinfodict():
def remote_initconfig(option_dict, args): def remote_initconfig(option_dict, args):
from _pytest.config import Config from _pytest.config import Config
option_dict['plugins'].append("no:terminal")
option_dict["plugins"].append("no:terminal")
config = Config.fromdictargs(option_dict, args) config = Config.fromdictargs(option_dict, args)
config.option.looponfail = False config.option.looponfail = False
config.option.usepdb = False config.option.usepdb = False
@@ -183,20 +192,24 @@ def remote_initconfig(option_dict, args):
return config return config
if __name__ == '__channelexec__': if __name__ == "__channelexec__":
channel = channel # noqa channel = channel # noqa
slaveinput, args, option_dict = channel.receive() workerinput, args, option_dict = channel.receive()
importpath = os.getcwd() importpath = os.getcwd()
sys.path.insert(0, importpath) # XXX only for remote situations sys.path.insert(0, importpath) # XXX only for remote situations
os.environ['PYTHONPATH'] = ( os.environ["PYTHONPATH"] = (
importpath + os.pathsep + importpath + os.pathsep + os.environ.get("PYTHONPATH", "")
os.environ.get('PYTHONPATH', '')) )
os.environ['PYTEST_XDIST_WORKER'] = slaveinput['slaveid'] os.environ["PYTEST_XDIST_WORKER"] = workerinput["workerid"]
os.environ['PYTEST_XDIST_WORKER_COUNT'] = str(slaveinput['slavecount']) os.environ["PYTEST_XDIST_WORKER_COUNT"] = str(workerinput["workercount"])
# os.environ['PYTHONPATH'] = importpath # os.environ['PYTHONPATH'] = importpath
import py import py
config = remote_initconfig(option_dict, args) config = remote_initconfig(option_dict, args)
config.slaveinput = slaveinput config.workerinput = workerinput
config.slaveoutput = {} config.workeroutput = {}
interactor = SlaveInteractor(config, channel) # TODO: deprecated name, backward compatibility only. Remove it in future
config.slaveinput = config.workerinput
config.slaveoutput = config.workeroutput
interactor = WorkerInteractor(config, channel)
config.hook.pytest_cmdline_main(config=config) config.hook.pytest_cmdline_main(config=config)

View File

@@ -1,4 +1,4 @@
import py from __future__ import print_function
from difflib import unified_diff from difflib import unified_diff
@@ -11,16 +11,11 @@ def report_collection_diff(from_collection, to_collection, from_id, to_id):
if from_collection == to_collection: if from_collection == to_collection:
return None return None
diff = unified_diff( diff = unified_diff(from_collection, to_collection, fromfile=from_id, tofile=to_id)
from_collection, error_message = (
to_collection, u"Different tests were collected between {from_id} and {to_id}. "
fromfile=from_id, u"The difference is:\n"
tofile=to_id, u"{diff}"
) ).format(from_id=from_id, to_id=to_id, diff="\n".join(diff))
error_message = py.builtin._totext(
'Different tests were collected between {from_id} and {to_id}. '
'The difference is:\n'
'{diff}'
).format(from_id=from_id, to_id=to_id, diff='\n'.join(diff))
msg = "\n".join([x.rstrip() for x in error_message.split("\n")]) msg = "\n".join([x.rstrip() for x in error_message.split("\n")])
return msg return msg

View File

@@ -1,10 +1,10 @@
from py.log import Producer from py.log import Producer
from xdist.slavemanage import parse_spec_config from xdist.workermanage import parse_spec_config
from xdist.report import report_collection_diff from xdist.report import report_collection_diff
class EachScheduling: class EachScheduling(object):
"""Implement scheduling of test items on all nodes """Implement scheduling of test items on all nodes
If a node gets added after the test run is started then it is If a node gets added after the test run is started then it is
@@ -86,10 +86,12 @@ class EachScheduling:
if deadnode.gateway.spec == node.gateway.spec: if deadnode.gateway.spec == node.gateway.spec:
dead_collection = self.node2collection[deadnode] dead_collection = self.node2collection[deadnode]
if collection != dead_collection: if collection != dead_collection:
msg = report_collection_diff(dead_collection, msg = report_collection_diff(
collection, dead_collection,
deadnode.gateway.id, collection,
node.gateway.id) deadnode.gateway.id,
node.gateway.id,
)
self.log(msg) self.log(msg)
return return
pending = self._removed2pending.pop(deadnode) pending = self._removed2pending.pop(deadnode)

View File

@@ -20,10 +20,10 @@ class LoadFileScheduling(LoadScopeScheduling):
This class behaves very much like LoadScopeScheduling, but with a file-level scope. This class behaves very much like LoadScopeScheduling, but with a file-level scope.
""" """
def __init(self, config, log=None): def __init__(self, config, log=None):
super(LoadFileScheduling, self).__init__(config, log) super(LoadFileScheduling, self).__init__(config, log)
if log is None: if log is None:
self.log = Producer('loadfilesched') self.log = Producer("loadfilesched")
else: else:
self.log = log.loadfilesched self.log = log.loadfilesched
@@ -49,4 +49,4 @@ class LoadFileScheduling(LoadScopeScheduling):
example/loadsuite/test/test_delta.py example/loadsuite/test/test_delta.py
example/loadsuite/epsilon/__init__.py example/loadsuite/epsilon/__init__.py
""" """
return nodeid.split('::', 1)[0] return nodeid.split("::", 1)[0]

View File

@@ -3,11 +3,11 @@ from itertools import cycle
from py.log import Producer from py.log import Producer
from _pytest.runner import CollectReport from _pytest.runner import CollectReport
from xdist.slavemanage import parse_spec_config from xdist.workermanage import parse_spec_config
from xdist.report import report_collection_diff from xdist.report import report_collection_diff
class LoadScheduling: class LoadScheduling(object):
"""Implement load scheduling across nodes. """Implement load scheduling across nodes.
This distributes the tests collected across all nodes so each test This distributes the tests collected across all nodes so each test
@@ -113,7 +113,7 @@ class LoadScheduling:
From now on the node will be allocated chunks of tests to From now on the node will be allocated chunks of tests to
execute. execute.
Called by the ``DSession.slave_slaveready`` hook when it Called by the ``DSession.worker_workerready`` hook when it
successfully bootstraps a new node. successfully bootstraps a new node.
""" """
assert node not in self.node2pending assert node not in self.node2pending
@@ -123,7 +123,7 @@ class LoadScheduling:
"""Add the collected test items from a node """Add the collected test items from a node
The collection is stored in the ``.node2collection`` map. The collection is stored in the ``.node2collection`` map.
Called by the ``DSession.slave_collectionfinish`` hook. Called by the ``DSession.worker_collectionfinish`` hook.
""" """
assert node in self.node2pending assert node in self.node2pending
if self.collection_is_completed: if self.collection_is_completed:
@@ -133,10 +133,9 @@ class LoadScheduling:
assert self.collection assert self.collection
if collection != self.collection: if collection != self.collection:
other_node = next(iter(self.node2collection.keys())) other_node = next(iter(self.node2collection.keys()))
msg = report_collection_diff(self.collection, msg = report_collection_diff(
collection, self.collection, collection, other_node.gateway.id, node.gateway.id
other_node.gateway.id, )
node.gateway.id)
self.log(msg) self.log(msg)
return return
self.node2collection[node] = list(collection) self.node2collection[node] = list(collection)
@@ -147,7 +146,7 @@ class LoadScheduling:
The duration it took to execute the item is used as a hint to The duration it took to execute the item is used as a hint to
the scheduler. the scheduler.
This is called by the ``DSession.slave_testreport`` hook. This is called by the ``DSession.worker_testreport`` hook.
""" """
self.node2pending[node].remove(item_index) self.node2pending[node].remove(item_index)
self.check_schedule(node, duration=duration) self.check_schedule(node, duration=duration)
@@ -187,8 +186,8 @@ class LoadScheduling:
This should be called either when the node crashed or at This should be called either when the node crashed or at
shutdown time. In the former case any pending items assigned shutdown time. In the former case any pending items assigned
to the node will be re-scheduled. Called by the to the node will be re-scheduled. Called by the
``DSession.slave_slavefinished`` and ``DSession.worker_workerfinished`` and
``DSession.slave_errordown`` hooks. ``DSession.worker_errordown`` hooks.
Return the item which was being executing while the node Return the item which was being executing while the node
crashed or None if the node has no more pending items. crashed or None if the node has no more pending items.
@@ -213,7 +212,7 @@ class LoadScheduling:
``.check_schedule()`` on all nodes so that newly added nodes ``.check_schedule()`` on all nodes so that newly added nodes
will start to be used. will start to be used.
This is called by the ``DSession.slave_collectionfinish`` hook This is called by the ``DSession.worker_collectionfinish`` hook
if ``.collection_is_completed`` is True. if ``.collection_is_completed`` is True.
""" """
assert self.collection_is_completed assert self.collection_is_completed
@@ -226,7 +225,7 @@ class LoadScheduling:
# XXX allow nodes to have different collections # XXX allow nodes to have different collections
if not self._check_nodes_have_same_collection(): if not self._check_nodes_have_same_collection():
self.log('**Different tests collected, aborting run**') self.log("**Different tests collected, aborting run**")
return return
# Collections are identical, create the index of pending items. # Collections are identical, create the index of pending items.
@@ -238,8 +237,7 @@ class LoadScheduling:
# Send a batch of tests to run. If we don't have at least two # Send a batch of tests to run. If we don't have at least two
# tests per node, we have to send them all so that we can send # tests per node, we have to send them all so that we can send
# shutdown signals and get all nodes working. # shutdown signals and get all nodes working.
initial_batch = max(len(self.pending) // 4, initial_batch = max(len(self.pending) // 4, 2 * len(self.nodes))
2 * len(self.nodes))
# distribute tests round-robin up to the batch size # distribute tests round-robin up to the batch size
# (or until we run out) # (or until we run out)
@@ -271,18 +269,15 @@ class LoadScheduling:
same_collection = True same_collection = True
for node, collection in node_collection_items[1:]: for node, collection in node_collection_items[1:]:
msg = report_collection_diff( msg = report_collection_diff(
col, col, collection, first_node.gateway.id, node.gateway.id
collection,
first_node.gateway.id,
node.gateway.id,
) )
if msg: if msg:
same_collection = False same_collection = False
self.log(msg) self.log(msg)
if self.config is not None: if self.config is not None:
rep = CollectReport( rep = CollectReport(
node.gateway.id, 'failed', node.gateway.id, "failed", longrepr=msg, result=[]
longrepr=msg, result=[]) )
self.config.hook.pytest_collectreport(report=rep) self.config.hook.pytest_collectreport(report=rep)
return same_collection return same_collection

View File

@@ -3,10 +3,10 @@ from collections import OrderedDict
from _pytest.runner import CollectReport from _pytest.runner import CollectReport
from py.log import Producer from py.log import Producer
from xdist.report import report_collection_diff from xdist.report import report_collection_diff
from xdist.slavemanage import parse_spec_config from xdist.workermanage import parse_spec_config
class LoadScopeScheduling: class LoadScopeScheduling(object):
"""Implement load scheduling across nodes, but grouping test by scope. """Implement load scheduling across nodes, but grouping test by scope.
This distributes the tests collected across all nodes so each test is run This distributes the tests collected across all nodes so each test is run
@@ -93,7 +93,7 @@ class LoadScopeScheduling:
self.registered_collections = OrderedDict() self.registered_collections = OrderedDict()
if log is None: if log is None:
self.log = Producer('loadscopesched') self.log = Producer("loadscopesched")
else: else:
self.log = log.loadscopesched self.log = log.loadscopesched
@@ -151,7 +151,7 @@ class LoadScopeScheduling:
From now on the node will be assigned work units to be executed. From now on the node will be assigned work units to be executed.
Called by the ``DSession.slave_slaveready`` hook when it successfully Called by the ``DSession.worker_workerready`` hook when it successfully
bootstraps a new node. bootstraps a new node.
""" """
assert node not in self.assigned_work assert node not in self.assigned_work
@@ -166,8 +166,8 @@ class LoadScopeScheduling:
Called by the hooks: Called by the hooks:
- ``DSession.slave_slavefinished``. - ``DSession.worker_workerfinished``.
- ``DSession.slave_errordown``. - ``DSession.worker_errordown``.
Return the item being executed while the node crashed or None if the Return the item being executed while the node crashed or None if the
node has no more pending items. node has no more pending items.
@@ -187,8 +187,7 @@ class LoadScopeScheduling:
break break
else: else:
raise RuntimeError( raise RuntimeError(
'Unable to identify crashitem on a workload with ' "Unable to identify crashitem on a workload with " "pending items"
'pending items'
) )
# Made uncompleted work unit available again # Made uncompleted work unit available again
@@ -206,7 +205,7 @@ class LoadScopeScheduling:
Called by the hook: Called by the hook:
- ``DSession.slave_collectionfinish``. - ``DSession.worker_collectionfinish``.
""" """
# Check that add_node() was called on the node before # Check that add_node() was called on the node before
@@ -224,10 +223,7 @@ class LoadScopeScheduling:
other_node = next(iter(self.registered_collections.keys())) other_node = next(iter(self.registered_collections.keys()))
msg = report_collection_diff( msg = report_collection_diff(
self.collection, self.collection, collection, other_node.gateway.id, node.gateway.id
collection,
other_node.gateway.id,
node.gateway.id
) )
self.log(msg) self.log(msg)
return return
@@ -239,7 +235,7 @@ class LoadScopeScheduling:
Called by the hook: Called by the hook:
- ``DSession.slave_testreport``. - ``DSession.worker_testreport``.
""" """
nodeid = self.registered_collections[node][item_index] nodeid = self.registered_collections[node][item_index]
scope = self._split_scope(nodeid) scope = self._split_scope(nodeid)
@@ -255,9 +251,7 @@ class LoadScopeScheduling:
scope, work_unit = self.workqueue.popitem(last=False) scope, work_unit = self.workqueue.popitem(last=False)
# Keep track of the assigned work # Keep track of the assigned work
assigned_to_node = self.assigned_work.setdefault( assigned_to_node = self.assigned_work.setdefault(node, default=OrderedDict())
node, default=OrderedDict()
)
assigned_to_node[scope] = work_unit assigned_to_node[scope] = work_unit
# Ask the node to execute the workload # Ask the node to execute the workload
@@ -292,14 +286,11 @@ class LoadScopeScheduling:
example/loadsuite/test/test_delta.py::Delta1 example/loadsuite/test/test_delta.py::Delta1
example/loadsuite/epsilon/__init__.py example/loadsuite/epsilon/__init__.py
""" """
return nodeid.rsplit('::', 1)[0] return nodeid.rsplit("::", 1)[0]
def _pending_of(self, workload): def _pending_of(self, workload):
"""Return the number of pending tests in a workload.""" """Return the number of pending tests in a workload."""
pending = sum( pending = sum(list(scope.values()).count(False) for scope in workload.values())
list(scope.values()).count(False)
for scope in workload.values()
)
return pending return pending
def _reschedule(self, node): def _reschedule(self, node):
@@ -317,7 +308,7 @@ class LoadScopeScheduling:
if not self.workqueue: if not self.workqueue:
return return
self.log('Number of units waiting for node:', len(self.workqueue)) self.log("Number of units waiting for node:", len(self.workqueue))
# Check that the node is almost depleted of work # Check that the node is almost depleted of work
# 2: Heuristic of minimum tests to enqueue more work # 2: Heuristic of minimum tests to enqueue more work
@@ -336,7 +327,7 @@ class LoadScopeScheduling:
If ``.collection_is_completed`` is True, this is called by the hook: If ``.collection_is_completed`` is True, this is called by the hook:
- ``DSession.slave_collectionfinish``. - ``DSession.worker_collectionfinish``.
""" """
assert self.collection_is_completed assert self.collection_is_completed
@@ -348,13 +339,11 @@ class LoadScopeScheduling:
# Check that all nodes collected the same tests # Check that all nodes collected the same tests
if not self._check_nodes_have_same_collection(): if not self._check_nodes_have_same_collection():
self.log('**Different tests collected, aborting run**') self.log("**Different tests collected, aborting run**")
return return
# Collections are identical, create the final list of items # Collections are identical, create the final list of items
self.collection = list( self.collection = list(next(iter(self.registered_collections.values())))
next(iter(self.registered_collections.values()))
)
if not self.collection: if not self.collection:
return return
@@ -368,18 +357,22 @@ class LoadScopeScheduling:
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 {0} 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 {0}".format(unused_node))
unused_node.shutdown() unused_node.shutdown()
# Assign initial workload # Assign initial workload
for node in self.nodes: for node in self.nodes:
self._assign_work_unit(node) self._assign_work_unit(node)
# Ensure nodes start with at least two work units if possible (#277)
for node in self.nodes:
self._reschedule(node)
# Initial distribution sent all tests, start node shutdown # Initial distribution sent all tests, start node shutdown
if not self.workqueue: if not self.workqueue:
for node in self.nodes: for node in self.nodes:
@@ -398,10 +391,7 @@ class LoadScopeScheduling:
for node, collection in node_collection_items[1:]: for node, collection in node_collection_items[1:]:
msg = report_collection_diff( msg = report_collection_diff(
col, col, collection, first_node.gateway.id, node.gateway.id
collection,
first_node.gateway.id,
node.gateway.id,
) )
if not msg: if not msg:
continue continue
@@ -412,12 +402,7 @@ class LoadScopeScheduling:
if self.config is None: if self.config is None:
continue continue
rep = CollectReport( rep = CollectReport(node.gateway.id, "failed", longrepr=msg, result=[])
node.gateway.id,
'failed',
longrepr=msg,
result=[]
)
self.config.hook.pytest_collectreport(report=rep) self.config.hook.pytest_collectreport(report=rep)
return same_collection return same_collection

View File

@@ -1,10 +1,13 @@
from __future__ import print_function
import fnmatch import fnmatch
import os import os
import re import re
import threading
import py import py
import pytest import pytest
import execnet import execnet
import xdist.remote import xdist.remote
from _pytest import runner # XXX load dynamically from _pytest import runner # XXX load dynamically
@@ -19,20 +22,21 @@ def parse_spec_config(config):
except ValueError: except ValueError:
xspeclist.append(xspec) xspeclist.append(xspec)
else: else:
xspeclist.extend([xspec[i + 1:]] * num) xspeclist.extend([xspec[i + 1 :]] * num)
if not xspeclist: if not xspeclist:
raise pytest.UsageError( raise pytest.UsageError(
"MISSING test execution (tx) nodes: please specify --tx") "MISSING test execution (tx) nodes: please specify --tx"
)
return xspeclist return xspeclist
class NodeManager(object): class NodeManager(object):
EXIT_TIMEOUT = 10 EXIT_TIMEOUT = 10
DEFAULT_IGNORES = ['.*', '*.pyc', '*.pyo', '*~'] DEFAULT_IGNORES = [".*", "*.pyc", "*.pyo", "*~"]
def __init__(self, config, specs=None, defaultchdir="pyexecnetcache"): def __init__(self, config, specs=None, defaultchdir="pyexecnetcache"):
self.config = config self.config = config
self._nodesready = py.std.threading.Event() self._nodesready = threading.Event()
self.trace = self.config.trace.get("nodemanager") self.trace = self.config.trace.get("nodemanager")
self.group = execnet.Group() self.group = execnet.Group()
if specs is None: if specs is None:
@@ -47,7 +51,7 @@ class NodeManager(object):
self.specs.append(spec) self.specs.append(spec)
self.roots = self._getrsyncdirs() self.roots = self._getrsyncdirs()
self.rsyncoptions = self._getrsyncoptions() self.rsyncoptions = self._getrsyncoptions()
self._rsynced_specs = py.builtin.set() self._rsynced_specs = set()
def rsync_roots(self, gateway): def rsync_roots(self, gateway):
"""Rsync the set of roots to the node's gateway cwd.""" """Rsync the set of roots to the node's gateway cwd."""
@@ -56,8 +60,7 @@ class NodeManager(object):
self.rsync(gateway, root, **self.rsyncoptions) self.rsync(gateway, root, **self.rsyncoptions)
def setup_nodes(self, putevent): def setup_nodes(self, putevent):
self.config.hook.pytest_xdist_setupnodes(config=self.config, self.config.hook.pytest_xdist_setupnodes(config=self.config, specs=self.specs)
specs=self.specs)
self.trace("setting up nodes") self.trace("setting up nodes")
nodes = [] nodes = []
for spec in self.specs: for spec in self.specs:
@@ -68,8 +71,8 @@ class NodeManager(object):
gw = self.group.makegateway(spec) gw = self.group.makegateway(spec)
self.config.hook.pytest_xdist_newgateway(gateway=gw) self.config.hook.pytest_xdist_newgateway(gateway=gw)
self.rsync_roots(gw) self.rsync_roots(gw)
node = SlaveController(self, gw, self.config, putevent) node = WorkerController(self, gw, self.config, putevent)
gw.node = node # keep the node alive gw.node = node # keep the node alive
node.setup() node.setup()
self.trace("started node %r" % node) self.trace("started node %r" % node)
return node return node
@@ -88,6 +91,7 @@ class NodeManager(object):
return [] return []
import pytest import pytest
import _pytest import _pytest
pytestpath = pytest.__file__.rstrip("co") pytestpath = pytest.__file__.rstrip("co")
pytestdir = py.path.local(_pytest.__file__).dirpath() pytestdir = py.path.local(_pytest.__file__).dirpath()
config = self.config config = self.config
@@ -111,10 +115,7 @@ class NodeManager(object):
ignores += self.config.option.rsyncignore ignores += self.config.option.rsyncignore
ignores += self.config.getini("rsyncignore") ignores += self.config.getini("rsyncignore")
return { return {"ignores": ignores, "verbose": self.config.option.verbose}
'ignores': ignores,
'verbose': self.config.option.verbose,
}
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."""
@@ -126,9 +127,12 @@ class NodeManager(object):
if spec.popen and not spec.chdir: if spec.popen and not spec.chdir:
# XXX This assumes that sources are python-packages # XXX This assumes that sources are python-packages
# and that adding the basedir does not hurt. # and that adding the basedir does not hurt.
gateway.remote_exec(""" gateway.remote_exec(
"""
import sys ; sys.path.insert(0, %r) import sys ; sys.path.insert(0, %r)
""" % os.path.dirname(str(source))).waitclose() """
% os.path.dirname(str(source))
).waitclose()
return return
if (spec, source) in self._rsynced_specs: if (spec, source) in self._rsynced_specs:
return return
@@ -136,28 +140,24 @@ class NodeManager(object):
def finished(): def finished():
if notify: if notify:
notify("rsyncrootready", spec, source) notify("rsyncrootready", spec, source)
rsync.add_target_host(gateway, finished=finished) rsync.add_target_host(gateway, finished=finished)
self._rsynced_specs.add((spec, source)) self._rsynced_specs.add((spec, source))
self.config.hook.pytest_xdist_rsyncstart( self.config.hook.pytest_xdist_rsyncstart(source=source, gateways=[gateway])
source=source,
gateways=[gateway],
)
rsync.send() rsync.send()
self.config.hook.pytest_xdist_rsyncfinish( self.config.hook.pytest_xdist_rsyncfinish(source=source, gateways=[gateway])
source=source,
gateways=[gateway],
)
class HostRSync(execnet.RSync): class HostRSync(execnet.RSync):
""" RSyncer that filters out common files """ RSyncer that filters out common files
""" """
def __init__(self, sourcedir, *args, **kwargs): def __init__(self, sourcedir, *args, **kwargs):
self._synced = {} self._synced = {}
self._ignores = [] self._ignores = []
ignores = kwargs.pop('ignores', None) or [] ignores = kwargs.pop("ignores", None) or []
for x in ignores: for x in ignores:
x = getattr(x, 'strpath', x) x = getattr(x, "strpath", x)
self._ignores.append(re.compile(fnmatch.translate(x))) self._ignores.append(re.compile(fnmatch.translate(x)))
super(HostRSync, self).__init__(sourcedir=sourcedir, **kwargs) super(HostRSync, self).__init__(sourcedir=sourcedir, **kwargs)
@@ -171,20 +171,19 @@ class HostRSync(execnet.RSync):
def add_target_host(self, gateway, finished=None): def add_target_host(self, gateway, finished=None):
remotepath = os.path.basename(self._sourcedir) remotepath = os.path.basename(self._sourcedir)
super(HostRSync, self).add_target(gateway, remotepath, super(HostRSync, self).add_target(
finishedcallback=finished, gateway, remotepath, finishedcallback=finished, delete=True
delete=True,) )
def _report_send_file(self, gateway, modified_rel_path): def _report_send_file(self, gateway, modified_rel_path):
if self._verbose: if self._verbose:
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
py.builtin.print_('%s:%s <= %s' % print("%s:%s <= %s" % (gateway.spec, remotepath, path))
(gateway.spec, remotepath, path))
def make_reltoroot(roots, args): def make_reltoroot(roots, args):
# XXX introduce/use public API for splitting py.test args # XXX introduce/use public API for splitting pytest args
splitcode = "::" splitcode = "::"
result = [] result = []
for arg in args: for arg in args:
@@ -201,7 +200,7 @@ def make_reltoroot(roots, args):
return result return result
class SlaveController(object): class WorkerController(object):
ENDMARK = -1 ENDMARK = -1
def __init__(self, nodemanager, gateway, config, putevent): def __init__(self, nodemanager, gateway, config, putevent):
@@ -209,23 +208,29 @@ class SlaveController(object):
self.putevent = putevent self.putevent = putevent
self.gateway = gateway self.gateway = gateway
self.config = config self.config = config
self.slaveinput = {'slaveid': gateway.id, self.workerinput = {
'slavecount': len(nodemanager.specs)} "workerid": gateway.id,
"workercount": len(nodemanager.specs),
"slaveid": gateway.id,
"slavecount": len(nodemanager.specs),
}
# TODO: deprecated name, backward compatibility only. Remove it in future
self.slaveinput = self.workerinput
self._down = False self._down = False
self._shutdown_sent = False self._shutdown_sent = False
self.log = py.log.Producer("slavectl-%s" % gateway.id) self.log = py.log.Producer("workerctl-%s" % gateway.id)
if not self.config.option.debug: if not self.config.option.debug:
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 "<%s %s>" % (self.__class__.__name__, self.gateway.id)
@property @property
def shutting_down(self): def shutting_down(self):
return self._down or self._shutdown_sent return self._down or self._shutdown_sent
def setup(self): def setup(self):
self.log("setting up slave session") self.log("setting up worker session")
spec = self.gateway.spec spec = self.gateway.spec
args = self.config.args args = self.config.args
if not spec.popen or spec.chdir: if not spec.popen or spec.chdir:
@@ -233,24 +238,22 @@ class SlaveController(object):
option_dict = vars(self.config.option) 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"):
basetemp = self.config._tmpdirhandler.getbasetemp() basetemp = self.config._tmpdirhandler.getbasetemp()
option_dict['basetemp'] = str(basetemp.join(name)) option_dict["basetemp"] = str(basetemp.join(name))
self.config.hook.pytest_configure_node(node=self) self.config.hook.pytest_configure_node(node=self)
self.channel = self.gateway.remote_exec(xdist.remote) self.channel = self.gateway.remote_exec(xdist.remote)
self.channel.send((self.slaveinput, args, option_dict)) self.channel.send((self.workerinput, args, option_dict))
if self.putevent: if self.putevent:
self.channel.setcallback( self.channel.setcallback(self.process_from_remote, endmarker=self.ENDMARK)
self.process_from_remote,
endmarker=self.ENDMARK)
def ensure_teardown(self): def ensure_teardown(self):
if hasattr(self, 'channel'): if hasattr(self, "channel"):
if not self.channel.isclosed(): if not self.channel.isclosed():
self.log("closing", self.channel) self.log("closing", self.channel)
self.channel.close() self.channel.close()
# del self.channel # del self.channel
if hasattr(self, 'gateway'): if hasattr(self, "gateway"):
self.log("exiting", self.gateway) self.log("exiting", self.gateway)
self.gateway.exit() self.gateway.exit()
# del self.gateway # del self.gateway
@@ -259,7 +262,7 @@ class SlaveController(object):
self.sendcommand("runtests", indices=indices) self.sendcommand("runtests", indices=indices)
def send_runtest_all(self): def send_runtest_all(self):
self.sendcommand("runtests_all",) self.sendcommand("runtests_all")
def shutdown(self): def shutdown(self):
if not self._down: if not self._down:
@@ -298,37 +301,42 @@ class SlaveController(object):
eventname, kwargs = eventcall eventname, kwargs = eventcall
if eventname in ("collectionstart",): if eventname in ("collectionstart",):
self.log("ignoring %s(%s)" % (eventname, kwargs)) self.log("ignoring %s(%s)" % (eventname, kwargs))
elif eventname == "slaveready": elif eventname == "workerready":
self.notify_inproc(eventname, node=self, **kwargs) self.notify_inproc(eventname, node=self, **kwargs)
elif eventname == "slavefinished": elif eventname == "workerfinished":
self._down = True self._down = True
self.slaveoutput = kwargs['slaveoutput'] self.workeroutput = self.slaveoutput = kwargs["workeroutput"]
self.notify_inproc("slavefinished", node=self) self.notify_inproc("workerfinished", node=self)
elif eventname in ("logstart", "logfinish"): elif eventname in ("logstart", "logfinish"):
self.notify_inproc(eventname, node=self, **kwargs) self.notify_inproc(eventname, node=self, **kwargs)
elif eventname in ( elif eventname in ("testreport", "collectreport", "teardownreport"):
"testreport", "collectreport", "teardownreport"):
item_index = kwargs.pop("item_index", None) item_index = kwargs.pop("item_index", None)
rep = unserialize_report(eventname, kwargs['data']) rep = unserialize_report(eventname, kwargs["data"])
if item_index is not None: if item_index is not None:
rep.item_index = item_index rep.item_index = item_index
self.notify_inproc(eventname, node=self, rep=rep) self.notify_inproc(eventname, node=self, rep=rep)
elif eventname == "collectionfinish": elif eventname == "collectionfinish":
self.notify_inproc(eventname, node=self, ids=kwargs['ids']) self.notify_inproc(eventname, node=self, ids=kwargs["ids"])
elif eventname == "runtest_protocol_complete": elif eventname == "runtest_protocol_complete":
self.notify_inproc(eventname, node=self, **kwargs) self.notify_inproc(eventname, node=self, **kwargs)
elif eventname == "logwarning": elif eventname == "logwarning":
self.notify_inproc(eventname, message=kwargs['message'], self.notify_inproc(
code=kwargs['code'], nodeid=kwargs['nodeid'], eventname,
fslocation=kwargs['nodeid']) message=kwargs["message"],
code=kwargs["code"],
nodeid=kwargs["nodeid"],
fslocation=kwargs["nodeid"],
)
else: else:
raise ValueError("unknown event: %s" % (eventname,)) raise ValueError("unknown event: %s" % (eventname,))
except KeyboardInterrupt: except KeyboardInterrupt:
# should not land in receiver-thread # should not land in receiver-thread
raise raise
except: # noqa except: # noqa
excinfo = py.code.ExceptionInfo() from _pytest._code import ExceptionInfo
py.builtin.print_("!" * 20, excinfo)
excinfo = ExceptionInfo()
print("!" * 20, excinfo)
self.config.notify_exception(excinfo) self.config.notify_exception(excinfo)
self.shutdown() self.shutdown()
self.notify_inproc("errordown", node=self, error=excinfo) self.notify_inproc("errordown", node=self, error=excinfo)
@@ -343,56 +351,56 @@ def unserialize_report(name, reportdict):
ReprFileLocation, ReprFileLocation,
ReprFuncArgs, ReprFuncArgs,
ReprLocals, ReprLocals,
ReprTraceback ReprTraceback,
) )
if reportdict['longrepr']:
if 'reprcrash' in reportdict['longrepr'] and 'reprtraceback' in reportdict['longrepr']:
reprtraceback = reportdict['longrepr']['reprtraceback'] if reportdict["longrepr"]:
reprcrash = reportdict['longrepr']['reprcrash'] if (
"reprcrash" in reportdict["longrepr"]
and "reprtraceback" in reportdict["longrepr"]
):
reprtraceback = reportdict["longrepr"]["reprtraceback"]
reprcrash = reportdict["longrepr"]["reprcrash"]
unserialized_entries = [] unserialized_entries = []
reprentry = None reprentry = None
for entry_data in reprtraceback['reprentries']: for entry_data in reprtraceback["reprentries"]:
data = entry_data['data'] data = entry_data["data"]
entry_type = entry_data['type'] entry_type = entry_data["type"]
if entry_type == 'ReprEntry': if entry_type == "ReprEntry":
reprfuncargs = None reprfuncargs = None
reprfileloc = None reprfileloc = None
reprlocals = None reprlocals = None
if data['reprfuncargs']: if data["reprfuncargs"]:
reprfuncargs = ReprFuncArgs( reprfuncargs = ReprFuncArgs(**data["reprfuncargs"])
**data['reprfuncargs']) if data["reprfileloc"]:
if data['reprfileloc']: reprfileloc = ReprFileLocation(**data["reprfileloc"])
reprfileloc = ReprFileLocation( if data["reprlocals"]:
**data['reprfileloc']) reprlocals = ReprLocals(data["reprlocals"]["lines"])
if data['reprlocals']:
reprlocals = ReprLocals(
data['reprlocals']['lines'])
reprentry = ReprEntry( reprentry = ReprEntry(
lines=data['lines'], lines=data["lines"],
reprfuncargs=reprfuncargs, reprfuncargs=reprfuncargs,
reprlocals=reprlocals, reprlocals=reprlocals,
filelocrepr=reprfileloc, filelocrepr=reprfileloc,
style=data['style'] style=data["style"],
) )
elif entry_type == 'ReprEntryNative': elif entry_type == "ReprEntryNative":
reprentry = ReprEntryNative(data['lines']) reprentry = ReprEntryNative(data["lines"])
else: else:
report_unserialization_failure( report_unserialization_failure(entry_type, name, reportdict)
entry_type, name, reportdict)
unserialized_entries.append(reprentry) unserialized_entries.append(reprentry)
reprtraceback['reprentries'] = unserialized_entries reprtraceback["reprentries"] = unserialized_entries
exception_info = ReprExceptionInfo( exception_info = ReprExceptionInfo(
reprtraceback=ReprTraceback(**reprtraceback), reprtraceback=ReprTraceback(**reprtraceback),
reprcrash=ReprFileLocation(**reprcrash), reprcrash=ReprFileLocation(**reprcrash),
) )
for section in reportdict['longrepr']['sections']: for section in reportdict["longrepr"]["sections"]:
exception_info.addsection(*section) exception_info.addsection(*section)
reportdict['longrepr'] = exception_info reportdict["longrepr"] = exception_info
return reportdict return reportdict
if name == "testreport": if name == "testreport":
@@ -403,13 +411,13 @@ def unserialize_report(name, reportdict):
def report_unserialization_failure(type_name, report_name, reportdict): def report_unserialization_failure(type_name, report_name, reportdict):
from pprint import pprint from pprint import pprint
url = 'https://github.com/pytest-dev/pytest-xdist/issues'
url = "https://github.com/pytest-dev/pytest-xdist/issues"
stream = py.io.TextIO() stream = py.io.TextIO()
pprint('-' * 100, stream=stream) pprint("-" * 100, stream=stream)
pprint('INTERNALERROR: Unknown entry type returned: %s' % type_name, pprint("INTERNALERROR: Unknown entry type returned: %s" % type_name, stream=stream)
stream=stream) pprint("report_name: %s" % report_name, stream=stream)
pprint('report_name: %s' % report_name, stream=stream)
pprint(reportdict, stream=stream) pprint(reportdict, stream=stream)
pprint('Please report this bug at %s' % url, stream=stream) pprint("Please report this bug at %s" % url, stream=stream)
pprint('-' * 100, stream=stream) pprint("-" * 100, stream=stream)
assert 0, stream.getvalue() assert 0, stream.getvalue()