Compare commits

...

142 Commits

Author SHA1 Message Date
Sven-Hendrik Haase
d3e1a52caa Prepare version 1.32.0 2020-05-03 01:00:40 +02:00
Bruno Oliveira
905eb3244d Merge pull request #528 from ashokdelphia/fix-acessing-typo-in-readme 2020-05-02 11:41:18 -03:00
Ashok Argent-Katwala
b4fb97a6e0 Fix typo in readme. 2020-05-02 11:40:23 -03:00
Sven-Hendrik Haase
e8bbb4db2a Merge pull request #525 from pytest-dev/524-add-global-session-uuid
Add global testrun uid
2020-05-01 19:36:53 +02:00
Sven-Hendrik Haase
069f959690 Fix docs format 2020-05-01 18:56:09 +02:00
Sven-Hendrik Haase
f56596bcdc Merge branch 'master' into 524-add-global-session-uuid 2020-05-01 16:56:23 +02:00
Sven-Hendrik Haase
ec13ceaa0c Merge pull request #527 from pytest-dev/ignore-python38-failure
Ignore Python 3.8 specific failure in pytest for now
2020-05-01 16:54:21 +02:00
Sven-Hendrik Haase
8714a99d51 Ignore Python 3.8 specific failure in pytest for now 2020-04-30 21:08:21 +02:00
Sven-Hendrik Haase
2183d741bb Add global session uuid (fixes #524) 2020-04-30 19:43:16 +02:00
Daniel Hahler
453cbd88dd tox/ci: remove testing with pytest's features branch (#509)
pytest is using the master branch now only.
2020-03-31 11:11:05 +02:00
Bruno Oliveira
bea6f66174 Merge pull request #475 from hugovk/rm-3.4
Drop support for EOL Python 3.4
2020-03-28 23:01:12 -03:00
Hugo van Kemenade
3a470043c9 Merge branch 'master' into rm-3.4 2020-01-26 21:05:51 +02:00
Bruno Oliveira
6fd5b56491 Merge pull request #495 from nicoddemus/release-1.31.0
Release 1.31.0
2019-12-19 22:45:30 -03:00
Bruno Oliveira
1f139a85d7 Release 1.31.0 2019-12-19 22:28:21 -03:00
Bruno Oliveira
1e284d31d0 Move 1.30.0 changelog to the top (by mistake?) 2019-12-19 22:25:42 -03:00
Ionel Cristian Mărieș
038a59eebc Fix regression with custom arguments being dropped in non-local… (#491)
* No idea what I'm doing.

* Extend integration test to cover ac3f6a78c7401aba9b62c2c67f280a129ae84eaf.

* Remove useless output to stop failing other tests.

* Reformat.

* Upgrade virtualenv to see if it fixes py34 builds

* Add CHANGELOG entry

* Make foobarplugin part of the test only

* Fix language_version in pre-commit for rst

* Fix test in Python 2

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

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

Fix #435
2019-06-07 09:35:22 -03:00
Bruno Oliveira
e165c2f3fb Use "nodes * 4" as default for --max-worker-restart (#436)
Use "nodes * 4" as default for --max-worker-restart
2019-06-07 09:19:24 -03:00
Bruno Oliveira
bbc5416037 Use "nodes * 4" as default for --max-worker-restart
Fix #226
2019-06-06 19:53:37 -03:00
Bruno Oliveira
a630e55887 Merge pull request #439 from nicoddemus/src-layout
Adopt 'src' layout and add 'testing' extras
2019-06-06 19:52:17 -03:00
Bruno Oliveira
7d41db3685 Adopt 'src' layout and add 'testing' extras 2019-06-06 19:35:13 -03:00
Bruno Oliveira
cdf422dec8 Improve CI: drop testing pytest master on py27, use py37 (#438)
Improve CI: drop testing pytest master on py27, use py37
2019-06-06 18:31:46 -03:00
Bruno Oliveira
abec5e60a2 Improve CI: drop testing pytest master on py27, use py37
* Use Python 3.7 as preferred Python 3 interpreter
* Do not test pytest `master` and `features` branches on
  Python 2.7, as those branches no longer support Python 2.7/3.4
2019-06-06 17:12:27 -03:00
Bruno Oliveira
9fcf8fa636 Merge pull request #430 from nicoddemus/release-1.28.0
Release 1.28.0
2019-04-03 18:12:01 -03:00
Bruno Oliveira
fa8037d365 Release 1.28.0 2019-04-02 20:14:47 -03:00
Bruno Oliveira
13efbfba69 Merge pull request #427 from blueyed/travis-xenial
ci: Travis: dist=xenial
2019-04-02 20:14:20 -03:00
Bruno Oliveira
0b840f8726 Merge pull request #429 from blueyed/cache-pre-commit
ci: Travis: cache for pre-commit (linting)
2019-04-02 20:13:43 -03:00
Bruno Oliveira
0242acfa0a Merge pull request #428 from blueyed/changedir
tox.ini: remove changedir, use pytest's testpaths
2019-04-02 20:13:00 -03:00
Bruno Oliveira
04c0265b6b Merge pull request #426 from nicoddemus/serialization-hooks
Serialization hooks (requires pytest 4.4)
2019-04-02 20:08:58 -03:00
Daniel Hahler
7ab9cf1a7a Apply suggestions from code review
Co-Authored-By: nicoddemus <nicoddemus@gmail.com>
2019-04-02 19:20:32 -03:00
Daniel Hahler
875aed976e ci: Travis: cache for pre-commit (linting) 2019-04-02 19:47:32 +02:00
Daniel Hahler
386c8eb416 tox.ini: remove changedir, use pytest's testpaths 2019-04-02 19:19:23 +02:00
Daniel Hahler
fc8e1133ac ci: Travis: dist=xenial 2019-04-02 19:09:37 +02:00
Bruno Oliveira
89d07a5cf2 Use serialization hooks from pytest 4.4 2019-04-01 18:37:03 -03:00
Bruno Oliveira
911dfdbcb6 Merge pull request #422 from blueyed/usepdb
Use default with usepdb option
2019-03-29 10:23:26 -03:00
Daniel Hahler
ba434848f5 Merge pull request #424 from blueyed/remove-_nodesready
Remove unuses NodeManager._nodesready
2019-03-26 14:40:00 +01:00
Daniel Hahler
8960ee45c6 Remove unuses NodeManager._nodesready
Apparently unused since 2efb059 (2010).
2019-03-23 22:10:57 +01:00
Daniel Hahler
9f6754f64a Use default with usepdb option
This is required with `-p no:debugging`.

Ref: https://github.com/pytest-dev/pytest/pull/4967
2019-03-21 05:31:24 +01:00
Bruno Oliveira
2bcbdb6745 Merge pull request #417 from nicoddemus/release-1.27.0
Release 1.27.0
2019-03-20 20:20:39 -03:00
Bruno Oliveira
d9c43a0933 Release 1.27.0 2019-02-15 12:17:48 -02:00
Bruno Oliveira
d753e53ee4 Merge pull request #374 from illiterati1/add_remote_hook
Add a hook to allow overriding the remote module that's executed by e…
2019-02-15 12:17:01 -02:00
Bruno Oliveira
81309e8d69 Adust linting and CHANGELOG 2019-02-15 09:35:06 -02:00
Bruno Oliveira
26d0fcabf6 Fix linting 2019-02-15 09:31:14 -02:00
Bruno Oliveira
486fa25bcb Merge branch 'master' into add_remote_hook 2019-02-15 09:23:32 -02:00
Bruno Oliveira
bc44fea8a8 Merge pull request #415 from jirikuncar/numprocesses-auto-with-pdb
Improve behavior of --numprocesses=auto and --pdb
2019-02-15 09:20:51 -02:00
Bruno Oliveira
6d544cef9a Fix linting 2019-02-15 08:37:57 -02:00
Jiri Kuncar
93780dcccc Simpler usage error in test_remote_usage_prog 2019-02-15 10:54:18 +01:00
Bruno Oliveira
ca58e5e7ed Fix test_remote_usage_prog after changes in pytest features branch 2019-02-13 15:20:32 -02:00
Jiri Kuncar
d8a794bc84 Improve behavior of -n=auto with --pdb option 2019-02-12 15:57:55 +01:00
Bruno Oliveira
ca032692a8 Merge pull request #407 from nicoddemus/pytest-logwarning-deprecation
Handle pytest_logwarning deprecation better and prepare next release
2019-01-28 18:52:09 -02:00
Bruno Oliveira
6fc02bb04a Release 1.26.1 2019-01-28 18:07:36 -02:00
Bruno Oliveira
c5f65b39ec Do not implement pytest_logwarning in pytest versions where it is deprecated
Fix #406
2019-01-28 18:05:41 -02:00
Bruno Oliveira
f00f9ecf58 Merge pull request #403 from BoboTiG/fix-resource-leaks
Fix ResourceWarning: unclosed file in setup.py
2019-01-15 08:22:08 -02:00
Ronny Pfannschmidt
48b7014530 Merge pull request #401 from nicoddemus/mainargv-docs
Add docs for sys.argv access from workers
2019-01-15 06:16:41 +01:00
Bruno Oliveira
1dec9dc246 ReprEntry objects no longer have the localssep attribute
The attribute was not used anywhere and has been removed in
pytest-dev/pytest#4615
2019-01-14 21:59:05 -02:00
Mickaël Schoentgen
f6ecaa9b7f Fix ResourceWarning: unclosed file in setup.py
Also uniformize the usage of the 'with' context manager.

Signed-off-by: Mickaël Schoentgen <contact@tiger-222.fr>
2019-01-14 22:59:36 +01:00
Bruno Oliveira
1276df4cfa Add docs for sys.argv access from workers 2019-01-11 21:59:33 -02:00
Bruno Oliveira
ccdb36a477 Merge pull request #400 from nicoddemus/release-1.26.0
Release 1.26.0
2019-01-11 21:14:46 -02:00
Bruno Oliveira
45ddc221ef Release 1.26.0 2019-01-11 15:19:37 -02:00
Bruno Oliveira
c3dbd05bd1 Merge pull request #379 from aadamson/warning-details-dumps-check
Check that we can serialize the warning detail attr
2019-01-11 15:17:02 -02:00
Bruno Oliveira
b9423d894c Fix linting 2019-01-10 19:19:34 -02:00
Alex Adamson
242c009866 Remove trailing whitespace 2019-01-10 15:50:25 -05:00
Alex Adamson
0ee219dc48 Address comments 2019-01-10 15:45:48 -05:00
bwilson
ad5e35213d moving py import 2019-01-09 16:17:57 -06:00
Bruno Oliveira
76075f5de1 Merge pull request #397 from nicoddemus/do-not-change-pythonpath
Do not change PYTHONPATH or sys.path on workers
2019-01-09 18:40:02 -02:00
Bruno Oliveira
f8d51aab57 Do not change PYTHONPATH or sys.path on local workers
Fix #376
2019-01-09 11:14:35 -02:00
Alex Adamson
ac2bf10fe2 Disable unserialized arguments check for py2 2019-01-08 15:17:03 -05:00
Alex Adamson
fa85371c73 Add test for #379 2019-01-08 15:17:03 -05:00
Alex Adamson
d55fab3aa7 Add changelog entry 2019-01-08 15:17:03 -05:00
Alex Adamson
4632df37a6 Check that we can serialize the warning detail attr 2019-01-08 15:17:03 -05:00
Bruno Oliveira
ca94bda1b7 Merge pull request #396 from nicoddemus/fix-4.1.0-tests
Skip config.warn tests in pytest 4.1
2019-01-08 17:18:13 -02:00
Bruno Oliveira
261d4189f0 Skip config.warn tests in pytest 4.1 2019-01-08 10:57:14 -02:00
Bruno Oliveira
f283dddf36 Merge pull request #392 from nicoddemus/release-1.25.0
Release 1.25.0
2018-12-13 11:09:27 -02:00
Bruno Oliveira
6d74bab518 Merge remote-tracking branch 'upstream/master' into release-1.25.0 2018-12-12 22:28:53 -02:00
Daniel Hahler
c306287a4e Merge pull request #393 from nicoddemus/fix-master
Fix test_remote_usage_prog which started to break with pytest@features
2018-12-12 23:15:45 +01:00
Bruno Oliveira
2b65800738 Fix test_remote_usage_prog which started to break with pytest@features 2018-12-12 18:25:04 -02:00
Bruno Oliveira
86bf3cb8b3 Release 1.25.0 2018-12-12 15:24:02 -02:00
Bruno Oliveira
95aaaec6c5 Merge pull request #388 from blueyed/pass-mainargv
Pass and use original sys.argv to/with workers
2018-12-12 15:21:56 -02:00
Bruno Oliveira
33d5441258 Merge pull request #390 from nicoddemus/pytest-logwarning
Prepare for the removal of pytest_logwarning in pytest 4.1
2018-12-12 08:59:21 -02:00
Bruno Oliveira
b5c09f8e84 Add CHANGELOG 2018-12-11 18:49:40 -02:00
Bruno Oliveira
a30124e858 Prepare for the removal of pytest_logwarning in pytest 4.1 2018-12-11 18:46:30 -02:00
Bruno Oliveira
c5d8635294 Merge pull request #375 from ethanhs/silencenodes
Don't show node setup when running quiet
2018-12-11 18:08:42 -02:00
Bruno Oliveira
5f0abe3cb8 Merge remote-tracking branch 'upstream/master' into ethanhs/silencenodes 2018-12-11 14:35:25 -02:00
Bruno Oliveira
3332c99e1e Improve verbosity test 2018-12-11 14:35:16 -02:00
Bruno Oliveira
3329bbbae2 Merge pull request #389 from nicoddemus/excinfo-fix-384
Fix ExceptionInfo usage for pytest 4.1
2018-12-11 14:23:21 -02:00
Bruno Oliveira
dd63d0b667 Merge pull request #386 from blueyed/minor
minor: fix error message and wrongly joined/blackified strings
2018-12-11 12:21:56 -02:00
Bruno Oliveira
dd6eac12a4 Fix ExceptionInfo usage for pytest 4.1
Fix #384
2018-12-11 12:19:34 -02:00
Daniel Hahler
a70dace69c Pass and use original sys.argv to/with workers
This gets used e.g. by argparse for the "prog" part.

We could explicitly pass it through and/or set it on
config._parser.prog, but that is a bit tedious just for this use case,
and it looks like "simulating" the main prog here appears to not be that
bad of a hack after all.
2018-12-11 06:41:18 +01:00
Daniel Hahler
6a21a5aa81 minor: fix error message and wrongly joined/blackified strings 2018-12-05 19:46:14 +01:00
Ethan Smith
49c09117ed Fix lint 2018-11-29 00:55:33 -08:00
Ethan Smith
566480fdd3 Add test and silence completely 2018-11-29 00:25:30 -08:00
bwilson
ab5f12f92e Revert "Adding RemoteTarget class to wrap module/function"
This reverts commit 311554324a.
2018-11-26 09:49:11 -06:00
bwilson
b28c3d98c5 Revert "linting"
This reverts commit b94dcfe4cd.
2018-11-26 09:49:10 -06:00
Ronny Pfannschmidt
5ecbf25450 Merge pull request #380 from nicoddemus/fix-master
Skip test which needs pytest_namespace(), removed recently
2018-11-22 07:11:19 +01:00
Ronny Pfannschmidt
646eb868ab Merge pull request #378 from tzickel/geventfix
fix a bug with gevent monkeypatch
2018-11-22 07:09:38 +01:00
Bruno Oliveira
50b27d2735 Skip test which needs pytest_namespace(), removed recently 2018-11-21 19:57:24 -02:00
tzickel
16db6eddd7 fix a bug with gevent monkeypatch 2018-11-21 18:59:28 +02:00
Ronny Pfannschmidt
f2061875e1 Merge pull request #377 from nicoddemus/pytest-4-fix
Update test that used deprecated cached_setup function
2018-11-16 19:35:04 +01:00
Bruno Oliveira
c4b365b082 Update test that used deprecated cached_setup function 2018-11-16 12:50:15 -02:00
bwilson
b94dcfe4cd linting 2018-11-13 10:45:40 -06:00
bwilson
311554324a Adding RemoteTarget class to wrap module/function 2018-11-13 10:37:37 -06:00
Ethan Smith
5465d936f7 Add newline 2018-11-12 14:05:07 -08:00
Ethan Smith
b819657dac Add changelog entry 2018-11-12 13:37:10 -08:00
Ethan Smith
d8c6c22989 Don't show node setup when running quiet 2018-11-12 13:31:19 -08:00
bwilson
5bb35b0f26 fixing linting problem with changelog 2018-11-12 13:53:35 -06:00
bwilson
47d271ad97 Adding news file 2018-11-12 13:41:18 -06:00
bwilson
7fb7bd9d04 Add a hook to allow overriding the remote module that's executed by execnet 2018-11-12 13:29:39 -06:00
Bruno Oliveira
1efec5a153 Merge pull request #372 from nicoddemus/drop-old-pytest
Drop support to old pytest versions and test with python 3.7
2018-11-11 12:15:34 -02:00
Bruno Oliveira
f9a13fe53b Drop support to old pytest versions and test with python 3.7 2018-11-11 11:27:30 -02:00
Bruno Oliveira
74a5e0ca03 Merge pull request #371 from nicoddemus/skip-module-370
Fix handling of skipped modules with pytest.skip at module level
2018-11-10 19:19:50 -02:00
Bruno Oliveira
3292e0ede0 Fix handling of skipped modules with pytest.skip at module level
Fix #332
2018-11-10 18:20:13 -02:00
Bruno Oliveira
d40d47acd9 Merge pull request #369 from nicoddemus/release-1.21.1
Release 1.21.1
2018-11-10 11:51:32 -02:00
30 changed files with 899 additions and 582 deletions

21
.appveyor.yml Normal file
View File

@@ -0,0 +1,21 @@
environment:
matrix:
- TOXENV: "py27-pytestlatest"
- TOXENV: "py35-pytestlatest"
- TOXENV: "py36-pytestlatest"
- TOXENV: "py37-pytestlatest"
- TOXENV: "py38-pytestlatest"
- TOXENV: "py38-pytestmaster"
install:
- C:\Python38\python -m pip install -U pip setuptools virtualenv
- C:\Python38\python -m pip install -U tox setuptools_scm
build: false # Not a C# project, build stuff at the test step instead.
test_script:
- C:\Python38\python -m tox
# We don't deploy anything on tags with AppVeyor, we use Travis instead, so we
# might as well save resources
skip_tags: true

2
.gitignore vendored
View File

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

View File

@@ -1,12 +1,12 @@
repos:
- repo: https://github.com/ambv/black
rev: 18.6b4
rev: 19.10b0
hooks:
- id: black
args: [--safe, --quiet]
language_version: python3.6
language_version: python3.7
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v1.3.0
rev: v2.4.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
@@ -21,4 +21,4 @@ repos:
files: ^(CHANGELOG.rst|HOWTORELEASE.rst|README.rst|changelog/.*)$
language: python
additional_dependencies: [pygments, restructuredtext_lint]
python_version: python3.6
language_version: python3.7

View File

@@ -1,59 +1,52 @@
sudo: false
dist: xenial
language: python
notifications:
irc:
channels:
- 'chat.freenode.net#pytest'
- 'chat.freenode.net#pytest'
on_success: change
on_failure: change
skip_join: true
email:
- pytest-commit@python.org
python:
- '2.7'
- '3.4'
- '3.5'
- '3.6'
env:
- TOXENV=py-pytest30
- TOXENV=py-pytest31
- TOXENV=py-pytest32
- TOXENV=py-pytest33
install: pip install tox setuptools_scm
install:
- pip install -U pip setuptools
- pip install tox setuptools_scm
script: tox
stages:
- baseline
- test
- name: deploy
if: repo = pytest-dev/pytest-xdist AND tag IS present
- baseline
- test
- name: deploy
if: repo = pytest-dev/pytest-xdist AND tag IS present
jobs:
include:
- stage: baseline
python: '3.6'
python: '3.7'
env: TOXENV=linting
- python: '3.6'
env: TOXENV=py36-pytestlatest
cache:
directories:
- $HOME/.cache/pre-commit
- python: '3.8'
env: TOXENV=py38-pytestlatest
- python: '2.7'
env: TOXENV=py27-pytestlatest
- stage: test
# python x env above are already included into this stage
- python: "2.7"
env: TOXENV=py27-pytestmaster
- python: "2.7"
env: TOXENV=py27-pytestfeatures
python: "3.5"
env: TOXENV=py35-pytestlatest
- python: "3.6"
env: TOXENV=py36-pytestmaster
- python: "3.6"
env: TOXENV=py36-pytestfeatures
env: TOXENV=py36-pytestlatest
- python: "3.7"
env: TOXENV=py37-pytestlatest
- python: "3.8"
env: TOXENV=py38-pytestmaster
- stage: deploy
python: '3.6'
python: '3.8'
env:
install: pip install -U setuptools setuptools_scm
script: skip

View File

@@ -1,3 +1,161 @@
pytest-xdist 1.32.0 (2020-05-03)
================================
Deprecations and Removals
-------------------------
- `#475 <https://github.com/pytest-dev/pytest-xdist/issues/475>`_: Drop support for EOL Python 3.4.
Features
--------
- `#524 <https://github.com/pytest-dev/pytest-xdist/issues/524>`_: Add `testrun_uid` fixture. This is a shared value that uniquely identifies a test run among all workers.
This also adds a `PYTEST_XDIST_TESTRUNUID` environment variable that is accessible within a test as well as a command line option `--testrunuid` to manually set the value from outside.
pytest-xdist 1.31.0 (2019-12-19)
================================
Features
--------
- `#486 <https://github.com/pytest-dev/pytest-xdist/issues/486>`_: Add support for Python 3.8.
Bug Fixes
---------
- `#491 <https://github.com/pytest-dev/pytest-xdist/issues/491>`_: Fix regression that caused custom plugin command-line arguments to be discarded when using ``--tx`` mode.
pytest-xdist 1.30.0 (2019-10-01)
================================
Features
--------
- `#448 <https://github.com/pytest-dev/pytest-xdist/issues/448>`_: Initialization between workers and master nodes is now more consistent, which fixes a number of
long-standing issues related to startup with the ``-c`` option.
Issues:
* `#6 <https://github.com/pytest-dev/pytest-xdist/issues/6>`__: Poor interaction between ``-n#`` and ``-c X.cfg``
* `#445 <https://github.com/pytest-dev/pytest-xdist/issues/445>`__: pytest-xdist is not reporting the same nodeid as pytest does
This however only works with **pytest 5.1 or later**, as it required changes in pytest itself.
Bug Fixes
---------
- `#467 <https://github.com/pytest-dev/pytest-xdist/issues/467>`_: Fix crash issues related to running xdist with the terminal plugin disabled.
pytest-xdist 1.29.0 (2019-06-14)
================================
Features
--------
- `#226 <https://github.com/pytest-dev/pytest-xdist/issues/226>`_: ``--max-worker-restart`` now assumes a more reasonable value (4 times the number of
nodes) when not given explicitly. This prevents test suites from running forever when the suite crashes during collection.
- `#435 <https://github.com/pytest-dev/pytest-xdist/issues/435>`_: When the test session is interrupted due to running out of workers, the reason is shown in the test summary
for easier viewing.
- `#442 <https://github.com/pytest-dev/pytest-xdist/issues/442>`_: Compatibility fix for upcoming pytest 5.0: ``session.exitstatus`` is now an ``IntEnum`` object.
Bug Fixes
---------
- `#435 <https://github.com/pytest-dev/pytest-xdist/issues/435>`_: No longer show an internal error when we run out of workers due to crashes.
pytest-xdist 1.28.0 (2019-04-02)
================================
Features
--------
- `#426 <https://github.com/pytest-dev/pytest-xdist/issues/426>`_: ``pytest-xdist`` now uses the new ``pytest_report_to_serializable`` and ``pytest_report_from_serializable``
hooks from ``pytest 4.4`` (still experimental). This will make report serialization more reliable and
extensible.
This also means that ``pytest-xdist`` now requires ``pytest>=4.4``.
pytest-xdist 1.27.0 (2019-02-15)
================================
Features
--------
- `#374 <https://github.com/pytest-dev/pytest-xdist/issues/374>`_: The new ``pytest_xdist_getremotemodule`` hook allows overriding the module run on remote nodes.
- `#415 <https://github.com/pytest-dev/pytest-xdist/issues/415>`_: Improve behavior of ``--numprocesses=auto`` to work well with ``--pdb`` option.
pytest-xdist 1.26.1 (2019-01-28)
================================
Bug Fixes
---------
- `#406 <https://github.com/pytest-dev/pytest-xdist/issues/406>`_: Do not implement deprecated ``pytest_logwarning`` hook in pytest versions where it is deprecated.
pytest-xdist 1.26.0 (2019-01-11)
================================
Features
--------
- `#376 <https://github.com/pytest-dev/pytest-xdist/issues/376>`_: The current directory is no longer added ``sys.path`` for local workers, only for remote connections.
This behavior is surprising because it makes xdist runs and non-xdist runs to potentially behave differently.
Bug Fixes
---------
- `#379 <https://github.com/pytest-dev/pytest-xdist/issues/379>`_: Warning attributes are checked to make sure they can be dumped prior to
serializing the warning for submission to the master node.
pytest-xdist 1.25.0 (2018-12-12)
================================
Deprecations and Removals
-------------------------
- `#372 <https://github.com/pytest-dev/pytest-xdist/issues/372>`_: Pytest versions older than 3.6 are no longer supported.
Features
--------
- `#373 <https://github.com/pytest-dev/pytest-xdist/issues/373>`_: Node setup information is hidden when pytest is run in quiet mode to reduce noise on many-core machines.
- `#388 <https://github.com/pytest-dev/pytest-xdist/issues/388>`_: ``mainargv`` is made available in ``workerinput`` from the host's ``sys.argv``.
This can be used via ``request.config.workerinput["mainargv"]``.
Bug Fixes
---------
- `#332 <https://github.com/pytest-dev/pytest-xdist/issues/332>`_: Fix report of module-level skips (``pytest.skip(reason, allow_module_level=True)``).
- `#378 <https://github.com/pytest-dev/pytest-xdist/issues/378>`_: Fix support for gevent monkeypatching
- `#384 <https://github.com/pytest-dev/pytest-xdist/issues/384>`_: pytest 4.1 support: ``ExceptionInfo`` API changes.
- `#390 <https://github.com/pytest-dev/pytest-xdist/issues/390>`_: pytest 4.1 support: ``pytest_logwarning`` hook removed.
pytest-xdist 1.24.1 (2018-11-09)
================================
@@ -298,17 +456,6 @@ Improved Documentation
- Added ``HOWTORELEASE`` documentation. (#155)
..
You should *NOT* be adding new change log entries to this file, this
file is managed by towncrier. You *may* edit previous change logs to
fix problems like typo corrections or such.
To add a new change log entry, please see
https://pip.pypa.io/en/latest/development/#adding-a-news-entry
We named the news folder ``changelog``
.. towncrier release notes start
1.17.0
------

View File

@@ -95,6 +95,59 @@ any guaranteed order, but you can control this with these options:
in version ``1.21``.
Making session-scoped fixtures execute only once
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
``pytest-xdist`` is designed so that each worker process will perform its own collection and execute
a subset of all tests. This means that tests in different processes requesting a high-level
scoped fixture (for example ``session``) will execute the fixture code more than once, which
breaks expectations and might be undesired in certain situations.
While ``pytest-xdist`` does not have a builtin support for ensuring a session-scoped fixture is
executed exactly once, this can be achieved by using a lock file for inter-process communication.
The example below needs to execute the fixture ``session_data`` only once (because it is
resource intensive, or needs to execute only once to define configuration options, etc), so it makes
use of a `FileLock <https://pypi.org/project/filelock/>`_ to produce the fixture data only once
when the first process requests the fixture, while the other processes will then read
the data from a file.
Here is the code:
.. code-block:: python
import json
import pytest
from filelock import FileLock
@pytest.fixture(scope="session")
def session_data(tmp_path_factory, worker_id):
if not worker_id:
# not executing in with multiple workers, just produce the data and let
# pytest's fixture caching do its job
return produce_expensive_data()
# get the temp directory shared by all workers
root_tmp_dir = tmp_path_factory.getbasetemp().parent
fn = root_tmp_dir / "data.json"
with FileLock(str(fn) + ".lock"):
if fn.is_file():
data = json.loads(fn.read_text())
else:
data = produce_expensive_data()
fn.write_text(json.dumps(data))
return data
The example above can also be use in cases a fixture needs to execute exactly once per test session, like
initializing a database service and populating initial tables.
This technique might not work for every case, but should be a starting point for many situations
where executing a high-scope fixture exactly once is important.
Running tests in a Python subprocess
------------------------------------
@@ -224,6 +277,47 @@ The information about the worker_id in a test is stored in the ``TestReport`` as
well, under the ``worker_id`` attribute.
Uniquely identifying the current test run
-----------------------------------------
*New in version 1.32.*
If you need to globally distinguish one test run from others in your
workers, you can use the ``testrun_uid`` fixture. For instance, let's say you
wanted to create a separate database for each test run:
.. code-block:: python
import pytest
from posix_ipc import Semaphore, O_CREAT
@pytest.fixture(scope="session", autouse=True)
def create_unique_database(testrun_uid):
""" create a unique database for this particular test run """
database_url = f"psql://myapp-{testrun_uid}"
with Semaphore(f"/{testrun_uid}-lock", flags=O_CREAT, initial_value=1):
if not database_exists(database_url):
create_database(database_url)
@pytest.fixture()
def db(testrun_uid):
""" retrieve unique database """
database_url = f"psql://myapp-{testrun_uid}"
return database_get_instance(database_url)
Additionally, during a test run, the following environment variable is defined:
* ``PYTEST_XDIST_TESTRUNUID``: the unique id of the test run.
Accessing ``sys.argv`` from the master node in workers
------------------------------------------------------
To access the ``sys.argv`` passed to the command-line of the master node, use
``request.config.workerinput["mainargv"]``.
Specifying test exec environments in an ini file
------------------------------------------------

View File

@@ -1,22 +0,0 @@
environment:
matrix:
# note: please use "tox --listenvs" to populate the build matrix
- TOXENV: "py27-pytest33"
- TOXENV: "py34-pytest33"
- TOXENV: "py35-pytest33"
- TOXENV: "py36-pytest33"
- TOXENV: "py36-pytestlatest"
- TOXENV: "py27-pytest33-pexpect"
- TOXENV: "py36-pytest33-pexpect"
install:
- C:\Python36\python -m pip install -U tox setuptools_scm pip
build: false # Not a C# project, build stuff at the test step instead.
test_script:
- 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

@@ -1,25 +1,30 @@
from setuptools import setup, find_packages
install_requires = ["execnet>=1.1", "pytest>=3.0.0", "pytest-forked", "six"]
install_requires = ["execnet>=1.1", "pytest>=4.4.0", "pytest-forked", "six"]
with open("README.rst") as f:
long_description = f.read()
setup(
name="pytest-xdist",
use_scm_version={"write_to": "xdist/_version.py"},
use_scm_version={"write_to": "src/xdist/_version.py"},
description="pytest xdist plugin for distributed testing"
" and loop-on-failing modes",
long_description=open("README.rst").read(),
long_description=long_description,
license="MIT",
author="holger krekel and contributors",
author_email="pytest-dev@python.org,holger@merlinux.eu",
url="https://github.com/pytest-dev/pytest-xdist",
platforms=["linux", "osx", "win32"],
packages=find_packages(exclude=["testing", "example"]),
packages=find_packages(where="src"),
package_dir={"": "src"},
extras_require={"testing": ["filelock"]},
entry_points={
"pytest11": ["xdist = xdist.plugin", "xdist.looponfail = xdist.looponfail"]
},
zip_safe=False,
python_requires=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*",
python_requires=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*",
install_requires=install_requires,
setup_requires=["setuptools_scm"],
classifiers=[
@@ -37,8 +42,9 @@ setup(
"Programming Language :: Python :: 2",
"Programming Language :: Python :: 2.7",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.4",
"Programming Language :: Python :: 3.5",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
],
)

View File

@@ -46,14 +46,11 @@ class DSession(object):
self._failed_collection_errors = {}
self._active_nodes = set()
self._failed_nodes_count = 0
self._max_worker_restart = self.config.option.maxworkerrestart
if self._max_worker_restart is not None:
self._max_worker_restart = int(self._max_worker_restart)
try:
self.terminal = config.pluginmanager.getplugin("terminalreporter")
except KeyError:
self.terminal = None
else:
self._max_worker_restart = get_default_max_worker_restart(self.config)
# summary message to print at the end of the session
self._summary_report = None
self.terminal = config.pluginmanager.getplugin("terminalreporter")
if self.terminal:
self.trdist = TerminalDistReporter(config)
config.pluginmanager.register(self.trdist, "terminaldistreporter")
@@ -172,7 +169,7 @@ class DSession(object):
"""
self.config.hook.pytest_testnodedown(node=node, error=None)
if node.workeroutput["exitstatus"] == 2: # keyboard-interrupt
self.shouldstop = "%s received keyboard-interrupt" % (node,)
self.shouldstop = "{} received keyboard-interrupt".format(node)
self.worker_errordown(node, "keyboard-interrupt")
return
if node in self.sched.nodes:
@@ -198,15 +195,23 @@ class DSession(object):
)
if maximum_reached:
if self._max_worker_restart == 0:
msg = "Worker restarting disabled"
msg = "worker {} crashed and worker restarting disabled".format(
node.gateway.id
)
else:
msg = "Maximum crashed workers reached: %d" % self._max_worker_restart
self.report_line(msg)
msg = "maximum crashed workers reached: %d" % self._max_worker_restart
self._summary_report = msg
self.report_line("\n" + msg)
self.triggershutdown()
else:
self.report_line("Replacing crashed worker %s" % node.gateway.id)
self.report_line("\nreplacing crashed worker %s" % node.gateway.id)
self._clone_node(node)
self._active_nodes.remove(node)
def pytest_terminal_summary(self, terminalreporter):
if self.config.option.verbose >= 0 and self._summary_report:
terminalreporter.write_sep("=", "xdist: {}".format(self._summary_report))
def worker_collectionfinish(self, node, ids):
"""worker has finished test collection.
@@ -229,9 +234,10 @@ class DSession(object):
if self.terminal and not self.sched.has_pending:
self.trdist.ensure_show_status()
self.terminal.write_line("")
self.terminal.write_line(
"scheduling tests via %s" % (self.sched.__class__.__name__)
)
if self.config.option.verbose > 0:
self.terminal.write_line(
"scheduling tests via %s" % (self.sched.__class__.__name__)
)
self.sched.schedule()
def worker_logstart(self, node, nodeid, location):
@@ -259,10 +265,10 @@ class DSession(object):
def worker_collectreport(self, node, rep):
"""Emitted when a node calls the pytest_collectreport hook.
Because we only need the report when there's a failure, as optimization
we only expect to receive failed reports from workers (#330).
Because we only need the report when there's a failure/skip, as optimization
we only expect to receive failed/skipped reports from workers (#330).
"""
assert rep.failed
assert not rep.passed
self._failed_worker_collectreport(node, rep)
def worker_logwarning(self, message, code, nodeid, fslocation):
@@ -315,7 +321,7 @@ class DSession(object):
# XXX count no of failures and retry N times
runner = self.config.pluginmanager.getplugin("runner")
fspath = nodeid.split("::")[0]
msg = "Worker %r crashed while running %r" % (worker.gateway.id, nodeid)
msg = "worker {!r} crashed while running {!r}".format(worker.gateway.id, nodeid)
rep = runner.TestReport(
nodeid, (fspath, None, fspath), (), "failed", msg, "???"
)
@@ -344,8 +350,13 @@ class TerminalDistReporter(object):
self.rewrite(self.getstatus())
def getstatus(self):
parts = ["%s %s" % (spec.id, self._status[spec.id]) for spec in self._specs]
return " / ".join(parts)
if self.config.option.verbose >= 0:
parts = [
"{} {}".format(spec.id, self._status[spec.id]) for spec in self._specs
]
return " / ".join(parts)
else:
return "bringing up nodes..."
def rewrite(self, line, newline=False):
pline = line + " " * max(self._lastlen - len(line), 0)
@@ -377,19 +388,27 @@ class TerminalDistReporter(object):
def pytest_testnodeready(self, node):
if self.config.option.verbose > 0:
d = node.workerinfo
infoline = "[%s] Python %s" % (d["id"], d["version"].replace("\n", " -- "))
infoline = "[{}] Python {}".format(
d["id"], d["version"].replace("\n", " -- ")
)
self.rewrite(infoline, newline=True)
self.setstatus(node.gateway.spec, "ok")
def pytest_testnodedown(self, node, error):
if not error:
return
self.write_line("[%s] node down: %s" % (node.gateway.id, error))
self.write_line("[{}] node down: {}".format(node.gateway.id, error))
# def pytest_xdist_rsyncstart(self, source, gateways):
# targets = ",".join([gw.id for gw in gateways])
# msg = "[%s] rsyncing: %s" %(targets, source)
# self.write_line(msg)
# def pytest_xdist_rsyncfinish(self, source, gateways):
# targets = ", ".join(["[%s]" % gw.id for gw in gateways])
# self.write_line("rsyncfinish: %s -> %s" %(source, targets))
def get_default_max_worker_restart(config):
"""gets the default value of --max-worker-restart option if it is not provided.
Use a reasonable default to avoid workers from restarting endlessly due to crashing collections (#226).
"""
result = config.option.maxworkerrestart
if result is not None:
result = int(result)
elif config.option.numprocesses:
# if --max-worker-restart was not provided, use a reasonable default (#226)
result = config.option.numprocesses * 4
return result

View File

@@ -30,9 +30,9 @@ def pytest_addoption(parser):
def pytest_cmdline_main(config):
if config.getoption("looponfail"):
usepdb = config.getoption("usepdb") # a core option
usepdb = config.getoption("usepdb", False) # a core option
if usepdb:
raise pytest.UsageError("--pdb incompatible with --looponfail.")
raise pytest.UsageError("--pdb is incompatible with --looponfail.")
looponfail_main(config)
return 2 # looponfail only can get stop with ctrl-C anyway
@@ -136,7 +136,7 @@ def repr_pytest_looponfailinfo(failreports, rootdirs):
tr.line(report, red=True)
tr.sep("#", "waiting for changes", bold=True)
for rootdir in rootdirs:
tr.line("### Watching: %s" % (rootdir,), bold=True)
tr.line("### Watching: {}".format(rootdir), bold=True)
def init_worker_session(channel, args, option_dict):

View File

@@ -30,6 +30,11 @@ def pytest_xdist_rsyncfinish(source, gateways):
""" called after rsyncing a directory to remote gateways takes place. """
@pytest.mark.firstresult
def pytest_xdist_getremotemodule():
""" called when creating remote node"""
def pytest_configure_node(node):
""" configure node information before it gets instantiated. """

View File

@@ -1,4 +1,5 @@
import os
import uuid
import py
import pytest
@@ -27,10 +28,14 @@ def auto_detect_cpus():
return n if n else 1
class AutoInt(int):
"""Mark value as auto-detected."""
def parse_numprocesses(s):
if s == "auto":
return auto_detect_cpus()
else:
return AutoInt(auto_detect_cpus())
elif s is not None:
return int(s)
@@ -45,7 +50,7 @@ def pytest_addoption(parser):
type=parse_numprocesses,
help="shortcut for '--dist=load --tx=NUM*popen', "
"you can use 'auto' here for auto detection CPUs number on "
"host system",
"host system and it will be 0 when used with --pdb",
)
group.addoption(
"--maxprocesses",
@@ -118,20 +123,30 @@ def pytest_addoption(parser):
metavar="GLOB",
help="add expression for ignores when rsyncing to remote tx nodes.",
)
group.addoption(
"--boxed",
action="store_true",
help="backward compatibility alias for pytest-forked --forked",
)
group.addoption(
"--testrunuid",
action="store",
help=(
"provide an identifier shared amongst all workers as the value of "
"the 'testrun_uid' fixture,\n\n,"
"if not provided, 'testrun_uid' is filled with a new unique string "
"on every test run."
),
)
parser.addini(
"rsyncdirs",
"list of (relative) paths to be rsynced for" " remote distributed testing.",
"list of (relative) paths to be rsynced for remote distributed testing.",
type="pathlist",
)
parser.addini(
"rsyncignore",
"list of (relative) glob-style paths to be ignored " "for rsyncing.",
"list of (relative) glob-style paths to be ignored for rsyncing.",
type="pathlist",
)
parser.addini(
@@ -150,11 +165,7 @@ def pytest_addoption(parser):
def pytest_addhooks(pluginmanager):
from xdist import newhooks
# avoid warnings with pytest-2.8
method = getattr(pluginmanager, "add_hookspecs", None)
if method is None:
method = pluginmanager.addhooks
method(newhooks)
pluginmanager.add_hookspecs(newhooks)
# -------------------------------------------------------------------------
@@ -170,13 +181,18 @@ def pytest_configure(config):
session = DSession(config)
config.pluginmanager.register(session, "dsession")
tr = config.pluginmanager.getplugin("terminalreporter")
tr.showfspath = False
if tr:
tr.showfspath = False
if config.getoption("boxed"):
config.option.forked = True
@pytest.mark.tryfirst
def pytest_cmdline_main(config):
usepdb = config.getoption("usepdb", False) # a core option
if isinstance(config.option.numprocesses, AutoInt):
config.option.numprocesses = 0 if usepdb else int(config.option.numprocesses)
if config.option.numprocesses:
if config.option.dist == "no":
config.option.dist = "load"
@@ -188,11 +204,10 @@ def pytest_cmdline_main(config):
config.option.dist = "load"
val = config.getvalue
if not val("collectonly"):
usepdb = config.getoption("usepdb") # a core option
if val("dist") != "no":
if usepdb:
raise pytest.UsageError(
"--pdb is incompatible with distributing tests; try using -n0."
"--pdb is incompatible with distributing tests; try using -n0 or -nauto."
) # noqa: E501
@@ -210,3 +225,12 @@ def worker_id(request):
return request.config.workerinput["workerid"]
else:
return "master"
@pytest.fixture(scope="session")
def testrun_uid(request):
"""Return the unique id of the current test."""
if hasattr(request.config, "workerinput"):
return request.config.workerinput["testrunuid"]
else:
return uuid.uuid4().hex

View File

@@ -10,15 +10,19 @@ import sys
import os
import time
import py
import _pytest.hookspec
import pytest
from execnet.gateway_base import dumps, DumpError
from _pytest.config import _prepareconfig, Config
class WorkerInteractor(object):
def __init__(self, config, channel):
self.config = config
self.workerid = config.workerinput.get("workerid", "?")
self.testrunuid = config.workerinput["testrunuid"]
self.log = py.log.Producer("worker-%s" % self.workerid)
if not config.option.debug:
py.log.setconsumer(self.log._keywords, None)
@@ -40,7 +44,8 @@ class WorkerInteractor(object):
@pytest.hookimpl(hookwrapper=True)
def pytest_sessionfinish(self, exitstatus):
self.config.workeroutput["exitstatus"] = exitstatus
# in pytest 5.0+, exitstatus is an IntEnum object
self.config.workeroutput["exitstatus"] = int(exitstatus)
yield
self.sendevent("workerfinished", workeroutput=self.config.workeroutput)
@@ -103,26 +108,36 @@ class WorkerInteractor(object):
self.sendevent("logfinish", nodeid=nodeid, location=location)
def pytest_runtest_logreport(self, report):
data = serialize_report(report)
data = self.config.hook.pytest_report_to_serializable(
config=self.config, report=report
)
data["item_index"] = self.item_index
data["worker_id"] = self.workerid
data["testrun_uid"] = self.testrunuid
assert self.session.items[self.item_index].nodeid == report.nodeid
self.sendevent("testreport", data=data)
def pytest_collectreport(self, report):
# master only needs reports that failed, as optimization send only them instead (#330)
if report.failed:
data = serialize_report(report)
# send only reports that have not passed to master as optimization (#330)
if not report.passed:
data = self.config.hook.pytest_report_to_serializable(
config=self.config, report=report
)
self.sendevent("collectreport", data=data)
def pytest_logwarning(self, message, code, nodeid, fslocation):
self.sendevent(
"logwarning",
message=message,
code=code,
nodeid=nodeid,
fslocation=str(fslocation),
)
# the pytest_logwarning hook was deprecated since pytest 4.0
if hasattr(
_pytest.hookspec, "pytest_logwarning"
) and not _pytest.hookspec.pytest_logwarning.pytest_spec.get("warn_on_impl"):
def pytest_logwarning(self, message, code, nodeid, fslocation):
self.sendevent(
"logwarning",
message=message,
code=code,
nodeid=nodeid,
fslocation=str(fslocation),
)
# the pytest_warning_captured hook was introduced in pytest 3.8
if hasattr(_pytest.hookspec, "pytest_warning_captured"):
@@ -137,47 +152,6 @@ class WorkerInteractor(object):
)
def serialize_report(rep):
def disassembled_report(rep):
reprtraceback = rep.longrepr.reprtraceback.__dict__.copy()
reprcrash = rep.longrepr.reprcrash.__dict__.copy()
new_entries = []
for entry in reprtraceback["reprentries"]:
entry_data = {"type": type(entry).__name__, "data": entry.__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)
reprtraceback["reprentries"] = new_entries
return {
"reprcrash": reprcrash,
"reprtraceback": reprtraceback,
"sections": rep.longrepr.sections,
}
import py
d = rep.__dict__.copy()
if hasattr(rep.longrepr, "toterminal"):
if hasattr(rep.longrepr, "reprtraceback") and hasattr(
rep.longrepr, "reprcrash"
):
d["longrepr"] = disassembled_report(rep)
else:
d["longrepr"] = str(rep.longrepr)
else:
d["longrepr"] = rep.longrepr
for name in d:
if isinstance(d[name], py.path.local):
d[name] = str(d[name])
elif name == "result":
d[name] = None # for now
return d
def serialize_warning_message(warning_message):
if isinstance(warning_message.message, Warning):
message_module = type(warning_message.message).__module__
@@ -215,7 +189,15 @@ def serialize_warning_message(warning_message):
for attr_name in warning_message._WARNING_DETAILS:
if attr_name in ("message", "category"):
continue
result[attr_name] = getattr(warning_message, attr_name)
attr = getattr(warning_message, attr_name)
# Check if we can serialize the warning detail, marking `None` otherwise
# Note that we need to define the attr (even as `None`) to allow deserializing
try:
dumps(attr)
except DumpError:
result[attr_name] = repr(attr)
else:
result[attr_name] = attr
return result
@@ -233,34 +215,43 @@ def getinfodict():
def remote_initconfig(option_dict, args):
from _pytest.config import Config
option_dict["plugins"].append("no:terminal")
config = Config.fromdictargs(option_dict, args)
return Config.fromdictargs(option_dict, args)
def setup_config(config, basetemp):
config.option.looponfail = False
config.option.usepdb = False
config.option.dist = "no"
config.option.distload = False
config.option.numprocesses = None
config.option.maxprocesses = None
config.args = args
return config
config.option.basetemp = basetemp
if __name__ == "__channelexec__":
channel = channel # noqa
workerinput, args, option_dict = channel.receive()
importpath = os.getcwd()
sys.path.insert(0, importpath) # XXX only for remote situations
os.environ["PYTHONPATH"] = (
importpath + os.pathsep + os.environ.get("PYTHONPATH", "")
)
workerinput, args, option_dict, change_sys_path = channel.receive()
if change_sys_path:
importpath = os.getcwd()
sys.path.insert(0, importpath)
os.environ["PYTHONPATH"] = (
importpath + os.pathsep + os.environ.get("PYTHONPATH", "")
)
os.environ["PYTEST_XDIST_TESTRUNUID"] = workerinput["testrunuid"]
os.environ["PYTEST_XDIST_WORKER"] = workerinput["workerid"]
os.environ["PYTEST_XDIST_WORKER_COUNT"] = str(workerinput["workercount"])
# os.environ['PYTHONPATH'] = importpath
import py
config = remote_initconfig(option_dict, args)
if hasattr(Config, "InvocationParams"):
config = _prepareconfig(args, None)
else:
config = remote_initconfig(option_dict, args)
config.args = args
setup_config(config, option_dict.get("basetemp"))
config._parser.prog = os.path.basename(workerinput["mainargv"][0])
config.workerinput = workerinput
config.workeroutput = {}
# TODO: deprecated name, backward compatibility only. Remove it in future

View File

@@ -187,7 +187,7 @@ class LoadScopeScheduling(object):
break
else:
raise RuntimeError(
"Unable to identify crashitem on a workload with " "pending items"
"Unable to identify crashitem on a workload with pending items"
)
# Made uncompleted work unit available again
@@ -358,12 +358,12 @@ class LoadScopeScheduling(object):
extra_nodes = len(self.nodes) - len(self.workqueue)
if extra_nodes > 0:
self.log("Shuting down {0} nodes".format(extra_nodes))
self.log("Shuting down {} nodes".format(extra_nodes))
for _ in range(extra_nodes):
unused_node, assigned = self.assigned_work.popitem(last=True)
self.log("Shuting down unused node {0}".format(unused_node))
self.log("Shuting down unused node {}".format(unused_node))
unused_node.shutdown()
# Assign initial workload

View File

@@ -2,7 +2,8 @@ from __future__ import print_function
import fnmatch
import os
import re
import threading
import sys
import uuid
import py
import pytest
@@ -10,8 +11,6 @@ import execnet
import xdist.remote
from _pytest import runner # XXX load dynamically
def parse_spec_config(config):
xspeclist = []
@@ -36,8 +35,10 @@ class NodeManager(object):
def __init__(self, config, specs=None, defaultchdir="pyexecnetcache"):
self.config = config
self._nodesready = threading.Event()
self.trace = self.config.trace.get("nodemanager")
self.testrunuid = self.config.getoption("testrunuid")
if self.testrunuid is None:
self.testrunuid = uuid.uuid4().hex
self.group = execnet.Group()
if specs is None:
specs = self._getxspecs()
@@ -104,7 +105,7 @@ class NodeManager(object):
for root in candidates:
root = py.path.local(root).realpath()
if not root.check():
raise pytest.UsageError("rsyncdir doesn't exist: %r" % (root,))
raise pytest.UsageError("rsyncdir doesn't exist: {!r}".format(root))
if root not in roots:
roots.append(root)
return roots
@@ -115,7 +116,10 @@ class NodeManager(object):
ignores += self.config.option.rsyncignore
ignores += self.config.getini("rsyncignore")
return {"ignores": ignores, "verbose": self.config.option.verbose}
return {
"ignores": ignores,
"verbose": getattr(self.config.option, "verbose", False),
}
def rsync(self, gateway, source, notify=None, verbose=False, ignores=None):
"""Perform rsync to remote hosts for node."""
@@ -179,7 +183,7 @@ class HostRSync(execnet.RSync):
if self._verbose:
path = os.path.basename(self._sourcedir) + "/" + modified_rel_path
remotepath = gateway.spec.chdir
print("%s:%s <= %s" % (gateway.spec, remotepath, path))
print("{}:{} <= {}".format(gateway.spec, remotepath, path))
def make_reltoroot(roots, args):
@@ -189,13 +193,16 @@ def make_reltoroot(roots, args):
for arg in args:
parts = arg.split(splitcode)
fspath = py.path.local(parts[0])
if not fspath.exists():
result.append(arg)
continue
for root in roots:
x = fspath.relto(root)
if x or fspath == root:
parts[0] = root.basename + "/" + x
break
else:
raise ValueError("arg %s not relative to an rsync root" % (arg,))
raise ValueError("arg {} not relative to an rsync root".format(arg))
result.append(splitcode.join(parts))
return result
@@ -203,7 +210,13 @@ def make_reltoroot(roots, args):
class WorkerController(object):
ENDMARK = -1
class RemoteHook:
@pytest.mark.trylast
def pytest_xdist_getremotemodule(self):
return xdist.remote
def __init__(self, nodemanager, gateway, config, putevent):
config.pluginmanager.register(self.RemoteHook())
self.nodemanager = nodemanager
self.putevent = putevent
self.gateway = gateway
@@ -213,6 +226,8 @@ class WorkerController(object):
"workercount": len(nodemanager.specs),
"slaveid": gateway.id,
"slavecount": len(nodemanager.specs),
"testrunuid": nodemanager.testrunuid,
"mainargv": sys.argv,
}
# TODO: deprecated name, backward compatibility only. Remove it in future
self.slaveinput = self.workerinput
@@ -223,7 +238,7 @@ class WorkerController(object):
py.log.setconsumer(self.log._keywords, None)
def __repr__(self):
return "<%s %s>" % (self.__class__.__name__, self.gateway.id)
return "<{} {}>".format(self.__class__.__name__, self.gateway.id)
@property
def shutting_down(self):
@@ -232,18 +247,27 @@ class WorkerController(object):
def setup(self):
self.log("setting up worker session")
spec = self.gateway.spec
args = self.config.args
if hasattr(self.config, "invocation_params"):
args = [str(x) for x in self.config.invocation_params.args or ()]
option_dict = {}
else:
args = self.config.args
option_dict = vars(self.config.option)
if not spec.popen or spec.chdir:
args = make_reltoroot(self.nodemanager.roots, args)
option_dict = vars(self.config.option)
if spec.popen:
name = "popen-%s" % self.gateway.id
if hasattr(self.config, "_tmpdirhandler"):
basetemp = self.config._tmpdirhandler.getbasetemp()
option_dict["basetemp"] = str(basetemp.join(name))
self.config.hook.pytest_configure_node(node=self)
self.channel = self.gateway.remote_exec(xdist.remote)
self.channel.send((self.workerinput, args, option_dict))
remote_module = self.config.hook.pytest_xdist_getremotemodule()
self.channel = self.gateway.remote_exec(remote_module)
# change sys.path only for remote workers
change_sys_path = not self.gateway.spec.popen
self.channel.send((self.workerinput, args, option_dict, change_sys_path))
if self.putevent:
self.channel.setcallback(self.process_from_remote, endmarker=self.ENDMARK)
@@ -268,17 +292,17 @@ class WorkerController(object):
if not self._down:
try:
self.sendcommand("shutdown")
except IOError:
except (IOError, OSError):
pass
self._shutdown_sent = True
def sendcommand(self, name, **kwargs):
""" send a named parametrized command to the other side. """
self.log("sending command %s(**%s)" % (name, kwargs))
self.log("sending command {}(**{})".format(name, kwargs))
self.channel.send((name, kwargs))
def notify_inproc(self, eventname, **kwargs):
self.log("queuing %s(**%s)" % (eventname, kwargs))
self.log("queuing {}(**{})".format(eventname, kwargs))
self.putevent((eventname, kwargs))
def process_from_remote(self, eventcall): # noqa too complex
@@ -300,7 +324,7 @@ class WorkerController(object):
return
eventname, kwargs = eventcall
if eventname in ("collectionstart",):
self.log("ignoring %s(%s)" % (eventname, kwargs))
self.log("ignoring {}({})".format(eventname, kwargs))
elif eventname == "workerready":
self.notify_inproc(eventname, node=self, **kwargs)
elif eventname == "workerfinished":
@@ -311,7 +335,9 @@ class WorkerController(object):
self.notify_inproc(eventname, node=self, **kwargs)
elif eventname in ("testreport", "collectreport", "teardownreport"):
item_index = kwargs.pop("item_index", None)
rep = unserialize_report(eventname, kwargs["data"])
rep = self.config.hook.pytest_report_from_serializable(
config=self.config, data=kwargs["data"]
)
if item_index is not None:
rep.item_index = item_index
self.notify_inproc(eventname, node=self, rep=rep)
@@ -338,87 +364,24 @@ class WorkerController(object):
item=kwargs["item"],
)
else:
raise ValueError("unknown event: %s" % (eventname,))
raise ValueError("unknown event: {}".format(eventname))
except KeyboardInterrupt:
# should not land in receiver-thread
raise
except: # noqa
from _pytest._code import ExceptionInfo
excinfo = ExceptionInfo()
# ExceptionInfo API changed in pytest 4.1
if hasattr(ExceptionInfo, "from_current"):
excinfo = ExceptionInfo.from_current()
else:
excinfo = ExceptionInfo()
print("!" * 20, excinfo)
self.config.notify_exception(excinfo)
self.shutdown()
self.notify_inproc("errordown", node=self, error=excinfo)
def unserialize_report(name, reportdict):
def assembled_report(reportdict):
from _pytest._code.code import (
ReprEntry,
ReprEntryNative,
ReprExceptionInfo,
ReprFileLocation,
ReprFuncArgs,
ReprLocals,
ReprTraceback,
)
if reportdict["longrepr"]:
if (
"reprcrash" in reportdict["longrepr"]
and "reprtraceback" in reportdict["longrepr"]
):
reprtraceback = reportdict["longrepr"]["reprtraceback"]
reprcrash = reportdict["longrepr"]["reprcrash"]
unserialized_entries = []
reprentry = None
for entry_data in reprtraceback["reprentries"]:
data = entry_data["data"]
entry_type = entry_data["type"]
if entry_type == "ReprEntry":
reprfuncargs = None
reprfileloc = None
reprlocals = None
if data["reprfuncargs"]:
reprfuncargs = ReprFuncArgs(**data["reprfuncargs"])
if data["reprfileloc"]:
reprfileloc = ReprFileLocation(**data["reprfileloc"])
if data["reprlocals"]:
reprlocals = ReprLocals(data["reprlocals"]["lines"])
reprentry = ReprEntry(
lines=data["lines"],
reprfuncargs=reprfuncargs,
reprlocals=reprlocals,
filelocrepr=reprfileloc,
style=data["style"],
)
elif entry_type == "ReprEntryNative":
reprentry = ReprEntryNative(data["lines"])
else:
report_unserialization_failure(entry_type, name, reportdict)
unserialized_entries.append(reprentry)
reprtraceback["reprentries"] = unserialized_entries
exception_info = ReprExceptionInfo(
reprtraceback=ReprTraceback(**reprtraceback),
reprcrash=ReprFileLocation(**reprcrash),
)
for section in reportdict["longrepr"]["sections"]:
exception_info.addsection(*section)
reportdict["longrepr"] = exception_info
return reportdict
if name == "testreport":
return runner.TestReport(**assembled_report(reportdict))
elif name == "collectreport":
return runner.CollectReport(**assembled_report(reportdict))
def unserialize_warning_message(data):
import warnings
import importlib
@@ -459,17 +422,3 @@ def unserialize_warning_message(data):
kwargs[attr_name] = data[attr_name]
return warnings.WarningMessage(**kwargs)
def report_unserialization_failure(type_name, report_name, reportdict):
from pprint import pprint
url = "https://github.com/pytest-dev/pytest-xdist/issues"
stream = py.io.TextIO()
pprint("-" * 100, stream=stream)
pprint("INTERNALERROR: Unknown entry type returned: %s" % type_name, stream=stream)
pprint("report_name: %s" % report_name, stream=stream)
pprint(reportdict, stream=stream)
pprint("Please report this bug at %s" % url, stream=stream)
pprint("-" * 100, stream=stream)
assert 0, stream.getvalue()

View File

@@ -1,5 +1,6 @@
import os
import re
import sys
import textwrap
import py
@@ -114,6 +115,9 @@ class TestDistribution:
assert result.ret == 2
result.stdout.fnmatch_lines(["*Interrupted: stopping*1*", "*1 failed*"])
@pytest.mark.xfail(
reason="#527: Python 3.8 failure in pytest where testdir.tmpdir returns an unexpected value"
)
def test_basetemp_in_subprocesses(self, testdir):
p1 = testdir.makepyfile(
"""
@@ -185,16 +189,47 @@ class TestDistribution:
)
assert result.ret == 1
def test_distribution_rsyncdirs_example(self, testdir):
@pytest.mark.xfail(reason="#527: Ignore Python 3.8 failure for the time being")
def test_distribution_rsyncdirs_example(self, testdir, monkeypatch):
# use a custom plugin that has a custom command-line option to ensure
# this is propagated to workers (see #491)
testdir.makepyfile(
**{
"myplugin/src/foobarplugin.py": """
from __future__ import print_function
import os
import sys
import pytest
def pytest_addoption(parser):
parser.addoption("--foobar", action="store", dest="foobar_opt")
@pytest.mark.tryfirst
def pytest_load_initial_conftests(early_config):
opt = early_config.known_args_namespace.foobar_opt
print("--foobar=%s active! [%s]" % (opt, os.getpid()), file=sys.stderr)
"""
}
)
assert (testdir.tmpdir / "myplugin/src/foobarplugin.py").check(file=1)
monkeypatch.setenv(
"PYTHONPATH", str(testdir.tmpdir / "myplugin/src"), prepend=os.pathsep
)
source = testdir.mkdir("source")
dest = testdir.mkdir("dest")
subdir = source.mkdir("example_pkg")
subdir.ensure("__init__.py")
p = subdir.join("test_one.py")
p.write("def test_5():\n assert not __file__.startswith(%r)" % str(p))
result = testdir.runpytest(
result = testdir.runpytest_subprocess(
"-v",
"-d",
"-s",
"-pfoobarplugin",
"--foobar=123",
"--dist=load",
"--rsyncdir=%(subdir)s" % locals(),
"--tx=popen//chdir=%(dest)s" % locals(),
p,
@@ -208,6 +243,7 @@ class TestDistribution:
"*1 passed*",
]
)
result.stderr.fnmatch_lines(["--foobar=123 active! *"])
assert dest.join(subdir.basename).check(dir=1)
def test_backward_compatibility_worker_terminology(self, testdir):
@@ -358,6 +394,30 @@ class TestDistEach:
class TestTerminalReporting:
@pytest.mark.parametrize("verbosity", ["", "-q", "-v"])
def test_output_verbosity(self, testdir, verbosity):
testdir.makepyfile(
"""
def test_ok():
pass
"""
)
args = ["-n1"]
if verbosity:
args.append(verbosity)
result = testdir.runpytest(*args)
out = result.stdout.str()
if verbosity == "-v":
assert "scheduling tests" in out
assert "gw" in out
elif verbosity == "-q":
assert "scheduling tests" not in out
assert "gw" not in out
assert "bringing up nodes..." in out
else:
assert "scheduling tests" not in out
assert "gw" in out
def test_pass_skip_fail(self, testdir):
testdir.makepyfile(
"""
@@ -465,9 +525,8 @@ def test_session_hooks(testdir):
name = "worker"
else:
name = "master"
f = open(name, "w")
f.write("xy")
f.close()
with open(name, "w") as f:
f.write("xy")
# let's fail on the worker
if name == "worker":
raise ValueError(42)
@@ -517,16 +576,15 @@ def test_session_testscollected(testdir):
assert collected_file.read() == "collected = 3"
def test_funcarg_teardown_failure(testdir):
def test_fixture_teardown_failure(testdir):
p = testdir.makepyfile(
"""
import pytest
@pytest.fixture
@pytest.fixture(scope="module")
def myarg(request):
def teardown(val):
raise ValueError(val)
return request.cached_setup(setup=lambda: 42, teardown=teardown,
scope="module")
yield 42
raise ValueError(42)
def test_hello(myarg):
pass
"""
@@ -536,6 +594,30 @@ def test_funcarg_teardown_failure(testdir):
assert result.ret
def test_config_initialization(testdir, pytestconfig):
"""Ensure workers and master are initialized consistently. Integration test for #445"""
if not hasattr(pytestconfig, "invocation_params"):
pytest.skip(
"requires pytest >=5.1 (config has no attribute 'invocation_params')"
)
testdir.makepyfile(
**{
"dir_a/test_foo.py": """
def test_1(): pass
"""
}
)
testdir.makefile(
".ini",
myconfig="""
[pytest]
testpaths=dir_a
""",
)
result = testdir.runpytest("-n2", "-c", "myconfig.ini", "-v")
result.stdout.fnmatch_lines(["dir_a/test_foo.py::test_1*"])
@pytest.mark.parametrize("when", ["setup", "call", "teardown"])
def test_crashing_item(testdir, when):
"""Ensure crashing item is correctly reported during all testing stages"""
@@ -612,6 +694,11 @@ def test_skipping(testdir):
def test_issue34_pluginloading_in_subprocess(testdir):
import _pytest.hookspec
if not hasattr(_pytest.hookspec, "pytest_namespace"):
pytest.skip("this pytest version no longer supports pytest_namespace()")
testdir.tmpdir.join("plugin123.py").write(
textwrap.dedent(
"""
@@ -708,22 +795,14 @@ def test_sub_plugins_disabled(testdir, plugin):
class TestWarnings:
@pytest.fixture(autouse=True)
def skip_if_unsupported_pytest_version(self):
"""Skip tests of this class if we are running in a pytest version which does not
support warnings yet.
"""
from pkg_resources import parse_version
if parse_version(pytest.__version__) < parse_version("3.1"):
pytest.skip("pytest warnings requires >= 3.1")
@pytest.mark.parametrize("n", ["-n0", "-n1"])
@pytest.mark.parametrize("warn_type", ["pytest", "builtin"])
def test_warnings(self, testdir, n, warn_type):
def test_warnings(self, testdir, n, request, warn_type):
if warn_type == "builtin":
warn_code = """warnings.warn(UserWarning('this is a warning'))"""
elif warn_type == "pytest":
if not hasattr(request.config, "warn"):
pytest.skip("config.warn has been removed in pytest 4.1")
warn_code = """request.config.warn('', 'this is a warning',
fslocation=py.path.local())"""
else:
@@ -740,7 +819,7 @@ class TestWarnings:
)
)
result = testdir.runpytest(n)
result.stdout.fnmatch_lines(["*this is a warning*", "*1 passed, 1 warnings*"])
result.stdout.fnmatch_lines(["*this is a warning*", "*1 passed, 1 warning*"])
@pytest.mark.parametrize("n", ["-n0", "-n1"])
def test_custom_subclass(self, testdir, n):
@@ -764,7 +843,7 @@ class TestWarnings:
)
testdir.syspathinsert()
result = testdir.runpytest(n)
result.stdout.fnmatch_lines(["*MyWarning*", "*1 passed, 1 warnings*"])
result.stdout.fnmatch_lines(["*MyWarning*", "*1 passed, 1 warning*"])
@pytest.mark.parametrize("n", ["-n0", "-n1"])
def test_unserializable_arguments(self, testdir, n):
@@ -781,7 +860,40 @@ class TestWarnings:
)
testdir.syspathinsert()
result = testdir.runpytest(n)
result.stdout.fnmatch_lines(["*UserWarning*foo.txt*", "*1 passed, 1 warnings*"])
result.stdout.fnmatch_lines(["*UserWarning*foo.txt*", "*1 passed, 1 warning*"])
@pytest.mark.parametrize("n", ["-n0", "-n1"])
def test_unserializable_warning_details(self, testdir, n):
"""Check that warnings with unserializable _WARNING_DETAILS are
handled correctly (#379).
"""
if sys.version_info[0] < 3:
# The issue is only present in Python 3 warnings
return
testdir.makepyfile(
"""
import warnings, pytest
import socket
import gc
def abuse_socket():
s = socket.socket()
del s
# Deliberately provoke a ResourceWarning for an unclosed socket.
# The socket itself will end up attached as a value in
# _WARNING_DETAIL. We need to test that it is not serialized
# (it can't be, so the test will fail if we try to).
@pytest.mark.filterwarnings('always')
def test_func(tmpdir):
abuse_socket()
gc.collect()
"""
)
testdir.syspathinsert()
result = testdir.runpytest(n)
result.stdout.fnmatch_lines(
["*ResourceWarning*unclosed*", "*1 passed, 1 warning*"]
)
class TestNodeFailure:
@@ -796,8 +908,8 @@ class TestNodeFailure:
res = testdir.runpytest(f, "-n1")
res.stdout.fnmatch_lines(
[
"*Replacing crashed worker*",
"*Worker*crashed while running*",
"replacing crashed worker gw*",
"worker*crashed while running*",
"*1 failed*1 passed*",
]
)
@@ -815,8 +927,8 @@ class TestNodeFailure:
res = testdir.runpytest(f, "-n2")
res.stdout.fnmatch_lines(
[
"*Replacing crashed worker*",
"*Worker*crashed while running*",
"replacing crashed worker gw*",
"worker*crashed while running*",
"*1 failed*3 passed*",
]
)
@@ -832,8 +944,8 @@ class TestNodeFailure:
res = testdir.runpytest(f, "--dist=each", "--tx=popen")
res.stdout.fnmatch_lines(
[
"*Replacing crashed worker*",
"*Worker*crashed while running*",
"replacing crashed worker gw*",
"worker*crashed while running*",
"*1 failed*1 passed*",
]
)
@@ -869,14 +981,35 @@ class TestNodeFailure:
res = testdir.runpytest(f, "-n4", "--max-worker-restart=1")
res.stdout.fnmatch_lines(
[
"*Replacing crashed worker*",
"*Maximum crashed workers reached: 1*",
"*Worker*crashed while running*",
"*Worker*crashed while running*",
"replacing crashed worker*",
"maximum crashed workers reached: 1*",
"worker*crashed while running*",
"worker*crashed while running*",
"*2 failed*2 passed*",
]
)
def test_max_worker_restart_tests_queued(self, testdir):
f = testdir.makepyfile(
"""
import os, pytest
@pytest.mark.parametrize('i', range(10))
def test(i): os._exit(1)
"""
)
res = testdir.runpytest(f, "-n2", "--max-worker-restart=3")
res.stdout.fnmatch_lines(
[
"replacing crashed worker*",
"maximum crashed workers reached: 3*",
"worker*crashed while running*",
"worker*crashed while running*",
"* xdist: maximum crashed workers reached: 3 *",
"* 4 failed in *",
]
)
assert "INTERNALERROR" not in res.stdout.str()
def test_max_worker_restart_die(self, testdir):
f = testdir.makepyfile(
"""
@@ -886,7 +1019,10 @@ class TestNodeFailure:
)
res = testdir.runpytest(f, "-n4", "--max-worker-restart=0")
res.stdout.fnmatch_lines(
["*Unexpectedly no active workers*", "*INTERNALERROR*"]
[
"* xdist: worker gw* crashed and worker restarting disabled *",
"* no tests ran in *",
]
)
def test_disable_restart(self, testdir):
@@ -901,9 +1037,10 @@ class TestNodeFailure:
res = testdir.runpytest(f, "-n4", "--max-worker-restart=0")
res.stdout.fnmatch_lines(
[
"*Worker restarting disabled*",
"*Worker*crashed while running*",
"*1 failed*2 passed*",
"worker gw* crashed and worker restarting disabled",
"*worker*crashed while running*",
"* xdist: worker gw* crashed and worker restarting disabled *",
"* 1 failed, 2 passed in *",
]
)
@@ -933,6 +1070,29 @@ def test_worker_id_fixture(testdir, n):
assert worker_ids == {"gw0", "gw1"}
@pytest.mark.parametrize("n", [0, 2])
def test_testrun_uid_fixture(testdir, n):
import glob
f = testdir.makepyfile(
"""
import pytest
@pytest.mark.parametrize("run_num", range(2))
def test_testrun_uid1(testrun_uid, run_num):
with open("testrun_uid%s.txt" % run_num, "w") as f:
f.write(testrun_uid)
"""
)
result = testdir.runpytest(f, "-n%d" % n)
result.stdout.fnmatch_lines("* 2 passed in *")
testrun_uids = set()
for fname in glob.glob(str(testdir.tmpdir.join("*.txt"))):
with open(fname) as f:
testrun_uids.add(f.read().strip())
assert len(testrun_uids) == 1
assert len(testrun_uids.pop()) == 32
@pytest.mark.parametrize("tb", ["auto", "long", "short", "no", "line", "native"])
def test_error_report_styles(testdir, tb):
testdir.makepyfile(
@@ -972,6 +1132,22 @@ def test_color_yes_collection_on_non_atty(testdir, request):
assert "gw0 C / gw1 C" not in result.stdout.str()
def test_without_terminal_plugin(testdir, request):
"""
No output when terminal plugin is disabled
"""
testdir.makepyfile(
"""
def test_1():
pass
"""
)
result = testdir.runpytest("-p", "no:terminal", "-n2")
assert result.stdout.str() == ""
assert result.stderr.str() == ""
assert result.ret == 0
def test_internal_error_with_maxfail(testdir):
"""
Internal error when using --maxfail option (#62, #65).

View File

@@ -1,20 +1,19 @@
import six
import py
import pytest
import execnet
@pytest.fixture(scope="session", autouse=True)
def _ensure_imports():
# we import some modules because pytest-2.8's testdir fixture
# will unload all modules after each test and this cause
# (unknown) problems with execnet.Group()
execnet.Group
execnet.makegateway
pytest_plugins = "pytester"
# rsyncdirs = ['.', '../xdist', py.path.local(execnet.__file__).dirpath()]
if six.PY2:
@pytest.fixture(scope="session", autouse=True)
def _ensure_imports():
# we import some modules because pytest-2.8's testdir fixture
# will unload all modules after each test and this cause
# (unknown) problems with execnet.Group()
execnet.Group
execnet.makegateway
@pytest.fixture(autouse=True)
@@ -45,14 +44,6 @@ def specssh(request):
return getspecssh(request.config)
@pytest.fixture
def testdir(testdir):
# pytest before 2.8 did not have a runpytest_subprocess
if not hasattr(testdir, "runpytest_subprocess"):
testdir.runpytest_subprocess = testdir.runpytest
return testdir
# configuration information for tests
def getgspecs(config):
return [execnet.XSpec(spec) for spec in config.getvalueorskip("gspecs")]

View File

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

View File

@@ -1,4 +1,7 @@
import py
import pytest
from pkg_resources import parse_version
from xdist.looponfail import RemoteControl
from xdist.looponfail import StatRecorder
@@ -214,7 +217,11 @@ class TestLooponFailing:
assert "test_one" not in remotecontrol.failures[0]
assert "test_two" in remotecontrol.failures[0]
@py.test.mark.xfail(py.test.__version__ >= "3.1", reason="broken by pytest 3.1+")
@pytest.mark.xfail(
parse_version(pytest.__version__) >= parse_version("3.1"),
reason="broken by pytest 3.1+",
strict=True,
)
def test_looponfail_removed_test(self, testdir):
modcol = testdir.getmodulecol(
"""

View File

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

View File

@@ -36,6 +36,7 @@ def test_dist_options(testdir):
def test_auto_detect_cpus(testdir, monkeypatch):
import os
from xdist.plugin import pytest_cmdline_main as check_options
if hasattr(os, "sched_getaffinity"):
monkeypatch.setattr(os, "sched_getaffinity", lambda _pid: set(range(99)))
@@ -52,6 +53,11 @@ def test_auto_detect_cpus(testdir, monkeypatch):
config = testdir.parseconfigure("-nauto")
assert config.getoption("numprocesses") == 99
config = testdir.parseconfigure("-nauto", "--pdb")
check_options(config)
assert config.getoption("usepdb")
assert config.getoption("numprocesses") == 0
monkeypatch.delattr(os, "sched_getaffinity", raising=False)
monkeypatch.setenv("TRAVIS", "true")
config = testdir.parseconfigure("-nauto")
@@ -89,6 +95,18 @@ def test_dsession_with_collect_only(testdir):
assert not config.pluginmanager.hasplugin("dsession")
def test_testrunuid_provided(testdir):
config = testdir.parseconfigure("--testrunuid", "test123", "--tx=popen")
nm = NodeManager(config)
assert nm.testrunuid == "test123"
def test_testrunuid_generated(testdir):
config = testdir.parseconfigure("--tx=popen")
nm = NodeManager(config)
assert len(nm.testrunuid) == 32
class TestDistOptions:
def test_getxspecs(self, testdir):
config = testdir.parseconfigure("--tx=popen", "--tx", "ssh=xyz")

View File

@@ -1,8 +1,10 @@
import py
import pprint
import pytest
from xdist.workermanage import WorkerController, unserialize_report
from xdist.remote import serialize_report
import sys
import uuid
from xdist.workermanage import WorkerController
import execnet
import marshal
@@ -24,7 +26,7 @@ class EventCall:
self.name, self.kwargs = eventcall
def __str__(self):
return "<EventCall %s(**%s)>" % (self.name, self.kwargs)
return "<EventCall {}(**{})>".format(self.name, self.kwargs)
class WorkerSetup:
@@ -43,6 +45,7 @@ class WorkerSetup:
putevent = self.use_callback and self.events.put or None
class DummyMananger:
testrunuid = uuid.uuid4().hex
specs = [0, 1]
self.slp = WorkerController(DummyMananger, self.gateway, config, putevent)
@@ -58,7 +61,7 @@ class WorkerSetup:
ev = EventCall(data)
if name is None or ev.name == name:
return ev
print("skipping %s" % (ev,))
print("skipping {}".format(ev))
def sendcommand(self, name, **kwargs):
self.slp.sendcommand(name, **kwargs)
@@ -79,188 +82,17 @@ def test_remoteinitconfig(testdir):
assert config2.pluginmanager.getplugin("terminal") in (-1, None)
class TestReportSerialization:
def test_xdist_longrepr_to_str_issue_241(self, testdir):
testdir.makepyfile(
"""
import os
def test_a(): assert False
def test_b(): pass
"""
)
testdir.makeconftest(
"""
def pytest_runtest_logreport(report):
print(report.longrepr)
"""
)
res = testdir.runpytest("-n1", "-s")
res.stdout.fnmatch_lines(["*1 failed, 1 passed *"])
def test_xdist_report_longrepr_reprcrash_130(self, testdir):
reprec = testdir.inline_runsource(
"""
import py
def test_fail(): assert False, 'Expected Message'
"""
)
reports = reprec.getreports("pytest_runtest_logreport")
assert len(reports) == 3
rep = reports[1]
added_section = ("Failure Metadata", str("metadata metadata"), "*")
rep.longrepr.sections.append(added_section)
d = serialize_report(rep)
check_marshallable(d)
a = unserialize_report("testreport", d)
# Check assembled == rep
assert a.__dict__.keys() == rep.__dict__.keys()
for key in rep.__dict__.keys():
if key != "longrepr":
assert getattr(a, key) == getattr(rep, key)
assert rep.longrepr.reprcrash.lineno == a.longrepr.reprcrash.lineno
assert rep.longrepr.reprcrash.message == a.longrepr.reprcrash.message
assert rep.longrepr.reprcrash.path == a.longrepr.reprcrash.path
assert rep.longrepr.reprtraceback.entrysep == a.longrepr.reprtraceback.entrysep
assert (
rep.longrepr.reprtraceback.extraline == a.longrepr.reprtraceback.extraline
)
assert rep.longrepr.reprtraceback.style == a.longrepr.reprtraceback.style
assert rep.longrepr.sections == a.longrepr.sections
# Missing section attribute PR171
assert added_section in a.longrepr.sections
def test_reprentries_serialization_170(self, testdir):
from _pytest._code.code import ReprEntry
reprec = testdir.inline_runsource(
"""
def test_repr_entry():
x = 0
assert x
""",
"--showlocals",
)
reports = reprec.getreports("pytest_runtest_logreport")
assert len(reports) == 3
rep = reports[1]
d = serialize_report(rep)
a = unserialize_report("testreport", d)
rep_entries = rep.longrepr.reprtraceback.reprentries
a_entries = a.longrepr.reprtraceback.reprentries
for i in range(len(a_entries)):
assert isinstance(rep_entries[i], ReprEntry)
assert rep_entries[i].lines == a_entries[i].lines
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.message == a_entries[i].reprfileloc.message
)
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].reprlocals.lines == a_entries[i].reprlocals.lines
assert rep_entries[i].style == a_entries[i].style
def test_reprentries_serialization_196(self, testdir):
from _pytest._code.code import ReprEntryNative
reprec = testdir.inline_runsource(
"""
def test_repr_entry_native():
x = 0
assert x
""",
"--tb=native",
)
reports = reprec.getreports("pytest_runtest_logreport")
assert len(reports) == 3
rep = reports[1]
d = serialize_report(rep)
a = unserialize_report("testreport", d)
rep_entries = rep.longrepr.reprtraceback.reprentries
a_entries = a.longrepr.reprtraceback.reprentries
for i in range(len(a_entries)):
assert isinstance(rep_entries[i], ReprEntryNative)
assert rep_entries[i].lines == a_entries[i].lines
def test_itemreport_outcomes(self, testdir):
reprec = testdir.inline_runsource(
"""
import py
def test_pass(): pass
def test_fail(): 0/0
@py.test.mark.skipif("True")
def test_skip(): pass
def test_skip_imperative():
py.test.skip("hello")
@py.test.mark.xfail("True")
def test_xfail(): 0/0
def test_xfail_imperative():
py.test.xfail("hello")
"""
)
reports = reprec.getreports("pytest_runtest_logreport")
assert len(reports) == 17 # with setup/teardown "passed" reports
for rep in reports:
d = serialize_report(rep)
check_marshallable(d)
newrep = unserialize_report("testreport", d)
assert newrep.passed == rep.passed
assert newrep.failed == rep.failed
assert newrep.skipped == rep.skipped
if newrep.skipped and not hasattr(newrep, "wasxfail"):
assert len(newrep.longrepr) == 3
assert newrep.outcome == rep.outcome
assert newrep.when == rep.when
assert newrep.keywords == rep.keywords
if rep.failed:
assert newrep.longreprtext == rep.longreprtext
def test_collectreport_passed(self, testdir):
reprec = testdir.inline_runsource("def test_func(): pass")
reports = reprec.getreports("pytest_collectreport")
for rep in reports:
d = serialize_report(rep)
check_marshallable(d)
newrep = unserialize_report("collectreport", d)
assert newrep.passed == rep.passed
assert newrep.failed == rep.failed
assert newrep.skipped == rep.skipped
def test_collectreport_fail(self, testdir):
reprec = testdir.inline_runsource("qwe abc")
reports = reprec.getreports("pytest_collectreport")
assert reports
for rep in reports:
d = serialize_report(rep)
check_marshallable(d)
newrep = unserialize_report("collectreport", d)
assert newrep.passed == rep.passed
assert newrep.failed == rep.failed
assert newrep.skipped == rep.skipped
if rep.failed:
assert newrep.longrepr == str(rep.longrepr)
def test_extended_report_deserialization(self, testdir):
reprec = testdir.inline_runsource("qwe abc")
reports = reprec.getreports("pytest_collectreport")
assert reports
for rep in reports:
rep.extra = True
d = serialize_report(rep)
check_marshallable(d)
newrep = unserialize_report("collectreport", d)
assert newrep.extra
assert newrep.passed == rep.passed
assert newrep.failed == rep.failed
assert newrep.skipped == rep.skipped
if rep.failed:
assert newrep.longrepr == str(rep.longrepr)
class TestWorkerInteractor:
def test_basic_collect_and_runtests(self, worker):
@pytest.fixture
def unserialize_report(self, pytestconfig):
def unserialize(data):
return pytestconfig.hook.pytest_report_from_serializable(
config=pytestconfig, data=data
)
return unserialize
def test_basic_collect_and_runtests(self, worker, unserialize_report):
worker.testdir.makepyfile(
"""
def test_func():
@@ -285,21 +117,18 @@ class TestWorkerInteractor:
ev = worker.popevent("testreport") # setup
ev = worker.popevent("testreport")
assert ev.name == "testreport"
rep = unserialize_report(ev.name, ev.kwargs["data"])
rep = unserialize_report(ev.kwargs["data"])
assert rep.nodeid.endswith("::test_func")
assert rep.passed
assert rep.when == "call"
ev = worker.popevent("workerfinished")
assert "workeroutput" in ev.kwargs
@pytest.mark.skipif(
pytest.__version__ >= "3.0", reason="skip at module level illegal in pytest 3.0"
)
def test_remote_collect_skip(self, worker):
def test_remote_collect_skip(self, worker, unserialize_report):
worker.testdir.makepyfile(
"""
import py
py.test.skip("hello")
import pytest
pytest.skip("hello", allow_module_level=True)
"""
)
worker.setup()
@@ -307,26 +136,25 @@ class TestWorkerInteractor:
assert not ev.kwargs
ev = worker.popevent()
assert ev.name == "collectreport"
ev = worker.popevent()
assert ev.name == "collectreport"
rep = unserialize_report(ev.name, ev.kwargs["data"])
rep = unserialize_report(ev.kwargs["data"])
assert rep.skipped
assert rep.longrepr[2] == "Skipped: hello"
ev = worker.popevent("collectionfinish")
assert not ev.kwargs["ids"]
def test_remote_collect_fail(self, worker):
def test_remote_collect_fail(self, worker, unserialize_report):
worker.testdir.makepyfile("""aasd qwe""")
worker.setup()
ev = worker.popevent("collectionstart")
assert not ev.kwargs
ev = worker.popevent()
assert ev.name == "collectreport"
rep = unserialize_report(ev.name, ev.kwargs["data"])
rep = unserialize_report(ev.kwargs["data"])
assert rep.failed
ev = worker.popevent("collectionfinish")
assert not ev.kwargs["ids"]
def test_runtests_all(self, worker):
def test_runtests_all(self, worker, unserialize_report):
worker.testdir.makepyfile(
"""
def test_func(): pass
@@ -348,7 +176,7 @@ class TestWorkerInteractor:
for i in range(3): # setup/call/teardown
ev = worker.popevent("testreport")
assert ev.name == "testreport"
rep = unserialize_report(ev.name, ev.kwargs["data"])
rep = unserialize_report(ev.kwargs["data"])
assert rep.nodeid.endswith(func)
ev = worker.popevent("workerfinished")
assert "workeroutput" in ev.kwargs
@@ -394,9 +222,71 @@ def test_remote_env_vars(testdir):
"""
import os
def test():
assert len(os.environ['PYTEST_XDIST_TESTRUNUID']) == 32
assert os.environ['PYTEST_XDIST_WORKER'] in ('gw0', 'gw1')
assert os.environ['PYTEST_XDIST_WORKER_COUNT'] == '2'
"""
)
result = testdir.runpytest("-n2", "--max-worker-restart=0")
assert result.ret == 0
def test_remote_inner_argv(testdir):
"""Test/document the behavior due to execnet using `python -c`."""
testdir.makepyfile(
"""
import sys
def test_argv():
assert sys.argv == ["-c"]
"""
)
result = testdir.runpytest("-n1")
assert result.ret == 0
def test_remote_mainargv(testdir):
outer_argv = sys.argv
testdir.makepyfile(
"""
def test_mainargv(request):
assert request.config.workerinput["mainargv"] == {!r}
""".format(
outer_argv
)
)
result = testdir.runpytest("-n1")
assert result.ret == 0
def test_remote_usage_prog(testdir, request):
if not hasattr(request.config._parser, "prog"):
pytest.skip("prog not available in config parser")
testdir.makeconftest(
"""
import pytest
config_parser = None
@pytest.fixture
def get_config_parser():
return config_parser
def pytest_configure(config):
global config_parser
config_parser = config._parser
"""
)
testdir.makepyfile(
"""
import sys
def test(get_config_parser, request):
get_config_parser._getparser().error("my_usage_error")
"""
)
result = testdir.runpytest_subprocess("-n1")
assert result.ret == 1
result.stdout.fnmatch_lines(["*usage: *", "*error: my_usage_error"])

View File

@@ -168,7 +168,7 @@ class TestNodeManager:
# assert nodemanager.config.topdir == source == config.topdir
nodemanager.makegateways()
nodemanager.rsync_roots()
p, = nodemanager.gwmanager.multi_exec(
(p,) = nodemanager.gwmanager.multi_exec(
"import os ; channel.send(os.getcwd())"
).receive_each()
p = py.path.local(p)
@@ -269,5 +269,5 @@ class TestNodeManager:
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

26
tox.ini
View File

@@ -1,30 +1,15 @@
[tox]
# if you change the envlist, please update .travis.yml file as well
envlist=
linting
py{27,34,35,36}-pytest{30,31,32,33,latest}
py{27,36}-pytest36-pexpect
py{27,36}-pytest{master,features}
py{27,35,36,37,38}-pytestlatest
py38-pytestmaster
[testenv]
changedir=testing
passenv = USER USERNAME
extras = testing
deps =
pycmd
# to avoid .eggs
setuptools_scm
pytest30: pytest~=3.0.5
pytest31: pytest~=3.1.0
pytest32: pytest~=3.2.0
pytest33: pytest~=3.3.0
pytestlatest: pytest
pytestmaster: git+https://github.com/pytest-dev/pytest.git@master
pytestfeatures: git+https://github.com/pytest-dev/pytest.git@features
pexpect: pexpect
filelock
platform=
pexpect: linux|darwin
commands=
pytest {posargs}
@@ -38,7 +23,7 @@ commands = pre-commit run --all-files --show-diff-on-failure
[testenv:release]
changedir=
decription = do a release, required posarg of the version number
basepython = python3.6
basepython = python3.7
skipsdist = True
usedevelop = True
passenv = *
@@ -48,7 +33,8 @@ commands =
towncrier --version {posargs} --yes
[pytest]
addopts = -rsfxX
addopts = -ra
testpaths = testing
[flake8]
max-line-length = 120