Compare commits

...

231 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
Bruno Oliveira
3579bfce7d Release 1.21.1 2018-11-09 14:29:21 -02:00
Ronny Pfannschmidt
365499eeaf Merge pull request #368 from nicoddemus/rename-filescope-file
Rename filescope to loadfile
2018-11-09 17:20:35 +01:00
Ronny Pfannschmidt
a518bd4e2a Merge pull request #362 from nicoddemus/serialize-fail-349
Handle warnings with unserializable arguments
2018-11-09 17:17:59 +01:00
Bruno Oliveira
d84ae73775 Rename filescope to loadfile
This makes it consistent with the option (--dist=loadfile) and the other
scheduling file (loadscope.py)
2018-11-09 13:00:40 -02:00
Bruno Oliveira
673fe9bd67 Create 'baseline' stage for fast-failure of builds, similar to pytest 2018-10-30 09:44:13 -03:00
Bruno Oliveira
9217b80fb1 Update CI to test against latest pytest release 2018-10-30 09:44:13 -03:00
Bruno Oliveira
b151e56fbd Handle unserializable warning arguments
Fix #349
2018-10-30 09:44:13 -03:00
Bruno Oliveira
70688b7986 Refactor warning acceptance tests into a class 2018-10-29 18:38:01 -03:00
Bruno Oliveira
adc385253c Merge pull request #357 from nicoddemus/release-1.24.0
Release 1.24.0
2018-10-29 18:23:37 -03:00
Bruno Oliveira
bfd6029102 Release 1.24.0 2018-10-18 07:55:54 -03:00
Bruno Oliveira
3f5a8eeb7f Merge pull request #356 from pristupa/maxprocessing-option
Add --maxprocesses option to use with --numprocesses=auto
2018-10-18 07:54:02 -03:00
Bruno Oliveira
d5b61e72d4 Merge pull request #351 from cybojanek/master
Issue node shutdown on empty workload
2018-10-18 07:53:52 -03:00
Bruno Oliveira
f224c4b00e Fix CHANGELOG formatting 2018-10-17 09:19:48 -03:00
Pavel Pristupa
ddefd3f453 Call group.addoption() instead of _addoption() when parsing command-line arguments
Fix #337
2018-10-17 17:48:47 +07:00
Pavel Pristupa
5ce8a6ec1a Add maxprocesses option to limit the maximum number of workers when using numprocesses=auto
Fix #337
2018-10-17 17:08:56 +07:00
Jan Kasiak
fe06f27b8b Issue node shutdown on empty workload
Remote pytest_runtestloop requires at least 2 tests, or a test
and a shutdown command. In case of inter test locking, for
example using a file lock, the tests could deadlock, since the
shutdown command would not be enqueued, and the last test of a
worker would never finish, and would not allow another worker
to make progress.
2018-09-30 15:39:04 -04:00
Bruno Oliveira
86e2fb5a5a Merge pull request #347 from nicoddemus/release-1.23.2
Release 1.23.2
2018-09-28 07:34:43 -03:00
Bruno Oliveira
ec6129639e Release 1.23.2 2018-09-28 06:38:50 -03:00
Bruno Oliveira
f6ac209cd5 Merge pull request #345 from BrandonHoffman/master
add exception handeling for warning serialization issues
2018-09-28 06:38:14 -03:00
Bruno Oliveira
b176b37606 Add a test which reproduces #344 and fix the handling code
Fix #344
2018-09-27 22:18:52 -03:00
Brandon Hoffman
22e36c9cb4 run black formatter 2018-09-27 19:32:10 -05:00
Brandon Hoffman
8825e7e234 use module name and class name for method instead of the reference to them 2018-09-27 19:27:49 -05:00
Brandon Hoffman
5d5f87b600 add moduel and class name to warning text 2018-09-27 19:24:58 -05:00
Brandon Hoffman
571d08fb8b remove unneeded lines 2018-09-26 21:05:30 -05:00
Brandon Hoffman
36c28ee154 add test case for invalid warning handeling 2018-09-26 20:44:01 -05:00
Brandon Hoffman
af3e975664 add changelog 2018-09-26 19:46:28 -05:00
Brandon Hoffman
bc2ba55f70 add exception handeling for warning serialization issues 2018-09-26 19:32:13 -05:00
Bruno Oliveira
8475876ce3 Merge pull request #343 from nicoddemus/release-1.23.1
Release 1.23.1
2018-09-25 17:05:40 -03:00
Bruno Oliveira
8ee6c0a4e6 Release 1.23.1 2018-09-25 12:11:08 -03:00
Bruno Oliveira
ec966d9800 Merge pull request #342 from nicoddemus/3.8-warnings
Fix warnings transfer between workers and master node with pytest >= 3.8
2018-09-25 12:10:10 -03:00
Bruno Oliveira
78030e3d32 Fix warnings transfer between workers and master node with pytest >= 3.8
Fix #341
2018-09-25 10:01:20 -03:00
Bruno Oliveira
ed2ab76b5c Merge pull request #334 from nicoddemus/release-1.23.0
Release 1.23.0
2018-08-25 01:49:04 -03:00
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
42 changed files with 2581 additions and 1637 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

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.
``` ```

3
.gitignore vendored
View File

@@ -23,12 +23,13 @@ include/
lib/ lib/
bin/ bin/
env/ env/
xdist/_version.py* src/xdist/_version.py*
pytest_xdist.egg-info pytest_xdist.egg-info
issue/ issue/
3rdparty/ 3rdparty/
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: 19.10b0
hooks:
- id: black
args: [--safe, --quiet]
language_version: python3.7
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v2.4.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]
language_version: python3.7

View File

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

View File

@@ -1,3 +1,264 @@
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)
================================
Bug Fixes
---------
- `#349 <https://github.com/pytest-dev/pytest-xdist/issues/349>`_: Correctly handle warnings created with arguments that can't be serialized during the transfer from workers to master node.
pytest-xdist 1.24.0 (2018-10-18)
================================
Features
--------
- `#337 <https://github.com/pytest-dev/pytest-xdist/issues/337>`_: New ``--maxprocesses`` command-line option that limits the maximum number of workers when using ``--numprocesses=auto``.
Bug Fixes
---------
- `#351 <https://github.com/pytest-dev/pytest-xdist/issues/351>`_: Fix scheduling deadlock in case of inter-test locking.
pytest-xdist 1.23.2 (2018-09-28)
================================
Bug Fixes
---------
- `#344 <https://github.com/pytest-dev/pytest-xdist/issues/344>`_: Fix issue where Warnings could cause pytest to fail if they do not set the args attribute correctly.
pytest-xdist 1.23.1 (2018-09-25)
================================
Bug Fixes
---------
- `#341 <https://github.com/pytest-dev/pytest-xdist/issues/341>`_: Fix warnings transfer between workers and master node with pytest >= 3.8.
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) pytest-xdist 1.22.1 (2018-02-19)
================================ ================================
@@ -195,17 +456,6 @@ Improved Documentation
- Added ``HOWTORELEASE`` documentation. (#155) - Added ``HOWTORELEASE`` documentation. (#155)
..
You should *NOT* be adding new change log entries to this file, this
file is managed by towncrier. You *may* edit previous change logs to
fix problems like typo corrections or such.
To add a new change log entry, please see
https://pip.pypa.io/en/latest/development/#adding-a-news-entry
We named the news folder ``changelog``
.. towncrier release notes start
1.17.0 1.17.0
------ ------

View File

@@ -24,13 +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 . -U -r dev-requirements.txt $ pip install tox
#. Update ``CHANGELOG.rst`` file by running:: #. Update the necessary files with::
$ towncrier --version X.Y.Z --yes $ tox -e release -- X.Y.Z
#. 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

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

@@ -74,5 +74,3 @@ serialize collected items to send them through the wire, as workers live in anot
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
.. image:: https://img.shields.io/badge/code%20style-black-000000.svg
:target: https://github.com/ambv/black
xdist: pytest distributed testing plugin xdist: pytest distributed testing plugin
======================================== ========================================
The `pytest-xdist`_ plugin extends py.test with some unique 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,14 +34,14 @@ 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.
@@ -67,7 +69,7 @@ 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 I/O 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
@@ -93,12 +95,65 @@ any guaranteed order, but you can control this with these options:
in version ``1.21``. in version ``1.21``.
Making session-scoped fixtures execute only once
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
``pytest-xdist`` is designed so that each worker process will perform its own collection and execute
a subset of all tests. This means that tests in different processes requesting a high-level
scoped fixture (for example ``session``) will execute the fixture code more than once, which
breaks expectations and might be undesired in certain situations.
While ``pytest-xdist`` does not have a builtin support for ensuring a session-scoped fixture is
executed exactly once, this can be achieved by using a lock file for inter-process communication.
The example below needs to execute the fixture ``session_data`` only once (because it is
resource intensive, or needs to execute only once to define configuration options, etc), so it makes
use of a `FileLock <https://pypi.org/project/filelock/>`_ to produce the fixture data only once
when the first process requests the fixture, while the other processes will then read
the data from a file.
Here is the code:
.. code-block:: python
import json
import pytest
from filelock import FileLock
@pytest.fixture(scope="session")
def session_data(tmp_path_factory, worker_id):
if not worker_id:
# not executing in with multiple workers, just produce the data and let
# pytest's fixture caching do its job
return produce_expensive_data()
# get the temp directory shared by all workers
root_tmp_dir = tmp_path_factory.getbasetemp().parent
fn = root_tmp_dir / "data.json"
with FileLock(str(fn) + ".lock"):
if fn.is_file():
data = json.loads(fn.read_text())
else:
data = produce_expensive_data()
fn.write_text(json.dumps(data))
return data
The example above can also be use in cases a fixture needs to execute exactly once per test session, like
initializing a database service and populating initial tables.
This technique might not work for every case, but should be a starting point for many situations
where executing a high-scope fixture exactly once is important.
Running tests in a Python subprocess Running tests in a Python subprocess
------------------------------------ ------------------------------------
To instantiate a python3.5 subprocess 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=python3.5 pytest -d --tx popen//python=python3.5
This will start a subprocess which is run with the ``python3.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.
@@ -129,10 +184,10 @@ 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
@@ -140,11 +195,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.
@@ -168,7 +223,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`:
@@ -180,7 +235,7 @@ 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
@@ -222,6 +277,47 @@ The information about the worker_id in a test is stored in the ``TestReport`` as
well, under the ``worker_id`` attribute. well, under the ``worker_id`` attribute.
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 Specifying test exec environments in an ini file
------------------------------------------------ ------------------------------------------------
@@ -242,7 +338,7 @@ You can also add default environments like this:
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.

View File

@@ -1,19 +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: "py27-pytest33-pexpect"
- TOXENV: "py36-pytest33-pexpect"
- TOXENV: "flakes"
- TOXENV: "readme"
install:
- C:\Python35\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:\Python35\python -m tox

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

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

@@ -1,47 +1,50 @@
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>=4.4.0", "pytest-forked", "six"]
with open("README.rst") as f:
long_description = f.read()
setup( setup(
name="pytest-xdist", name="pytest-xdist",
use_scm_version={'write_to': 'xdist/_version.py'}, use_scm_version={"write_to": "src/xdist/_version.py"},
description='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=long_description,
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(where="src"),
package_dir={"": "src"},
extras_require={"testing": ["filelock"]},
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.*, !=3.4.*",
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.5",
'Programming Language :: Python :: 3.5', "Programming Language :: Python :: 3.6",
'Programming Language :: Python :: 3.6', "Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
], ],
) )

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

@@ -10,15 +10,15 @@ 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
@@ -26,7 +26,7 @@ class DSession:
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,19 +41,16 @@ 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_worker_restart = self.config.option.maxworkerrestart self._max_worker_restart = get_default_max_worker_restart(self.config)
if self._max_worker_restart is not None: # summary message to print at the end of the session
self._max_worker_restart = int(self._max_worker_restart) self._summary_report = None
try:
self.terminal = config.pluginmanager.getplugin("terminalreporter") self.terminal = config.pluginmanager.getplugin("terminalreporter")
except KeyError: if self.terminal:
self.terminal = None
else:
self.trdist = TerminalDistReporter(config) self.trdist = TerminalDistReporter(config)
config.pluginmanager.register(self.trdist, "terminaldistreporter") config.pluginmanager.register(self.trdist, "terminaldistreporter")
@@ -84,7 +81,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 +92,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
@@ -129,7 +125,7 @@ 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
@@ -151,8 +147,8 @@ class DSession:
collection without any further input. collection without any further input.
""" """
node.workerinfo = workerinfo node.workerinfo = workerinfo
node.workerinfo['id'] = node.gateway.id node.workerinfo["id"] = node.gateway.id
node.workerinfo['spec'] = node.gateway.spec node.workerinfo["spec"] = node.gateway.spec
# TODO: (#234 task) needs this for pytest. Remove when refactor in pytest repo # TODO: (#234 task) needs this for pytest. Remove when refactor in pytest repo
node.slaveinfo = node.workerinfo node.slaveinfo = node.workerinfo
@@ -172,8 +168,8 @@ class DSession:
workerready 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.workeroutput['exitstatus'] == 2: # keyboard-interrupt if node.workeroutput["exitstatus"] == 2: # keyboard-interrupt
self.shouldstop = "%s received keyboard-interrupt" % (node,) self.shouldstop = "{} received keyboard-interrupt".format(node)
self.worker_errordown(node, "keyboard-interrupt") self.worker_errordown(node, "keyboard-interrupt")
return return
if node in self.sched.nodes: if node in self.sched.nodes:
@@ -193,20 +189,29 @@ 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_worker_restart is not None and maximum_reached = (
self._failed_nodes_count > self._max_worker_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_worker_restart == 0: if self._max_worker_restart == 0:
msg = 'Worker restarting disabled' msg = "worker {} crashed and worker restarting disabled".format(
node.gateway.id
)
else: else:
msg = "Maximum crashed workers reached: %d" % \ msg = "maximum crashed workers reached: %d" % self._max_worker_restart
self._max_worker_restart self._summary_report = msg
self.report_line(msg) self.report_line("\n" + msg)
self.triggershutdown()
else: else:
self.report_line("Replacing crashed worker %s" % node.gateway.id) self.report_line("\nreplacing crashed worker %s" % node.gateway.id)
self._clone_node(node) self._clone_node(node)
self._active_nodes.remove(node) self._active_nodes.remove(node)
def pytest_terminal_summary(self, terminalreporter):
if self.config.option.verbose >= 0 and self._summary_report:
terminalreporter.write_sep("=", "xdist: {}".format(self._summary_report))
def worker_collectionfinish(self, node, ids): def worker_collectionfinish(self, node, ids):
"""worker has finished test collection. """worker has finished test collection.
@@ -218,8 +223,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)
@@ -230,19 +234,19 @@ 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" % ( if self.config.option.verbose > 0:
self.sched.__class__.__name__)) self.terminal.write_line(
"scheduling tests via %s" % (self.sched.__class__.__name__)
)
self.sched.schedule() self.sched.schedule()
def worker_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 worker_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 worker_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."""
@@ -259,8 +263,12 @@ class DSession:
self.sched.mark_test_complete(node, item_index, duration) self.sched.mark_test_complete(node, item_index, duration)
def worker_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:
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 not rep.passed
self._failed_worker_collectreport(node, rep) self._failed_worker_collectreport(node, rep)
def worker_logwarning(self, message, code, nodeid, fslocation): def worker_logwarning(self, message, code, nodeid, fslocation):
@@ -268,6 +276,11 @@ class DSession:
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)
def worker_warning_captured(self, warning_message, when, item):
"""Emitted when a node calls the pytest_logwarning hook."""
kwargs = dict(warning_message=warning_message, when=when, item=item)
self.config.hook.pytest_warning_captured.call_historic(kwargs=kwargs)
def _clone_node(self, node): def _clone_node(self, node):
"""Return new node based on an existing one. """Return new node based on an existing one.
@@ -295,8 +308,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")
@@ -309,20 +321,21 @@ class DSession:
# XXX count no of failures and retry N times # XXX count no of failures and retry N times
runner = self.config.pluginmanager.getplugin("runner") runner = self.config.pluginmanager.getplugin("runner")
fspath = nodeid.split("::")[0] fspath = nodeid.split("::")[0]
msg = "Worker %r crashed while running %r" % (worker.gateway.id, nodeid) msg = "worker {!r} crashed while running {!r}".format(worker.gateway.id, nodeid)
rep = runner.TestReport(nodeid, (fspath, None, fspath), rep = runner.TestReport(
(), "failed", msg, "???") nodeid, (fspath, None, fspath), (), "failed", msg, "???"
)
rep.node = worker 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)
@@ -337,9 +350,13 @@ 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]) if self.config.option.verbose >= 0:
for spec in self._specs] parts = [
"{} {}".format(spec.id, self._status[spec.id]) for spec in self._specs
]
return " / ".join(parts) return " / ".join(parts)
else:
return "bringing up nodes..."
def rewrite(self, line, newline=False): def rewrite(self, line, newline=False):
pline = line + " " * max(self._lastlen - len(line), 0) pline = line + " " * max(self._lastlen - len(line), 0)
@@ -361,29 +378,37 @@ 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.workerinfo d = node.workerinfo
infoline = "[%s] Python %s" % ( infoline = "[{}] Python {}".format(
d['id'], d["id"], d["version"].replace("\n", " -- ")
d['version'].replace('\n', ' -- '),) )
self.rewrite(infoline, newline=True) self.rewrite(infoline, newline=True)
self.setstatus(node.gateway.spec, "ok") self.setstatus(node.gateway.spec, "ok")
def pytest_testnodedown(self, node, error): def pytest_testnodedown(self, node, error):
if not error: if not error:
return return
self.write_line("[%s] node down: %s" % (node.gateway.id, error)) self.write_line("[{}] node down: {}".format(node.gateway.id, error))
# def pytest_xdist_rsyncstart(self, source, gateways):
# targets = ",".join([gw.id for gw in gateways]) def get_default_max_worker_restart(config):
# msg = "[%s] rsyncing: %s" %(targets, source) """gets the default value of --max-worker-restart option if it is not provided.
# self.write_line(msg)
# def pytest_xdist_rsyncfinish(self, source, gateways): Use a reasonable default to avoid workers from restarting endlessly due to crashing collections (#226).
# targets = ", ".join(["[%s]" % gw.id for gw in gateways]) """
# self.write_line("rsyncfinish: %s -> %s" %(source, targets)) result = config.option.maxworkerrestart
if result is not None:
result = int(result)
elif config.option.numprocesses:
# if --max-worker-restart was not provided, use a reasonable default (#226)
result = config.option.numprocesses * 4
return result

View File

@@ -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", False) # a core option
if usepdb: if usepdb:
raise pytest.UsageError( raise pytest.UsageError("--pdb is 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,7 +71,7 @@ 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 worker session") self.trace("setting up worker session")
self.gateway = self.initgateway() self.gateway = self.initgateway()
@@ -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
@@ -131,14 +136,15 @@ def repr_pytest_looponfailinfo(failreports, rootdirs):
tr.line(report, red=True) tr.line(report, red=True)
tr.sep("#", "waiting for changes", bold=True) tr.sep("#", "waiting for changes", bold=True)
for rootdir in rootdirs: for rootdir in rootdirs:
tr.line("### Watching: %s" % (rootdir,), bold=True) tr.line("### Watching: {}".format(rootdir), bold=True)
def init_worker_session(channel, args, option_dict): def init_worker_session(channel, args, option_dict):
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_worker_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 WorkerFailSession from xdist.looponfail import WorkerFailSession
WorkerFailSession(config, channel).main() WorkerFailSession(config, channel).main()
class WorkerFailSession: 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 WorkerFailSession:
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
@@ -206,12 +215,12 @@ class WorkerFailSession:
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

@@ -30,6 +30,11 @@ def pytest_xdist_rsyncfinish(source, gateways):
""" called after rsyncing a directory to remote gateways takes place. """ """ 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): def pytest_configure_node(node):
""" configure node information before it gets instantiated. """ """ configure node information before it gets instantiated. """

236
src/xdist/plugin.py Normal file
View File

@@ -0,0 +1,236 @@
import os
import uuid
import py
import pytest
def auto_detect_cpus():
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:
from os import cpu_count
except ImportError:
from multiprocessing import cpu_count
else:
def cpu_count():
return len(sched_getaffinity(0))
try:
n = cpu_count()
except NotImplementedError:
return 1
return n if n else 1
class AutoInt(int):
"""Mark value as auto-detected."""
def parse_numprocesses(s):
if s == "auto":
return AutoInt(auto_detect_cpus())
elif s is not None:
return int(s)
def pytest_addoption(parser):
group = parser.getgroup("xdist", "distributed and subprocess testing")
group._addoption(
"-n",
"--numprocesses",
dest="numprocesses",
metavar="numprocesses",
action="store",
type=parse_numprocesses,
help="shortcut for '--dist=load --tx=NUM*popen', "
"you can use 'auto' here for auto detection CPUs number on "
"host system and it will be 0 when used with --pdb",
)
group.addoption(
"--maxprocesses",
dest="maxprocesses",
metavar="maxprocesses",
action="store",
type=int,
help="limit the maximum number of workers to process the tests when using --numprocesses=auto",
)
group.addoption(
"--max-worker-restart",
"--max-slave-restart",
action="store",
default=None,
dest="maxworkerrestart",
help="maximum number of workers that can be restarted "
"when crashed (set to zero to disable this feature)\n"
"'--max-slave-restart' option is deprecated and will be removed in "
"a future release",
)
group.addoption(
"--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",
help=(
"add a test execution environment. some examples: "
"--tx popen//python=python2.5 --tx socket=192.168.1.102:8888 "
"--tx ssh=user@codespeak.net//chdir=testcache"
),
)
group._addoption(
"-d",
action="store_true",
dest="distload",
default=False,
help="load-balance tests. shortcut for '--dist=load'",
)
group.addoption(
"--rsyncdir",
action="append",
default=[],
metavar="DIR",
help="add directory for rsyncing to remote tx nodes.",
)
group.addoption(
"--rsyncignore",
action="append",
default=[],
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.",
type="pathlist",
)
parser.addini(
"rsyncignore",
"list of (relative) glob-style paths to be ignored for rsyncing.",
type="pathlist",
)
parser.addini(
"looponfailroots",
type="pathlist",
help="directories to check for changes",
default=[py.path.local()],
)
# -------------------------------------------------------------------------
# distributed testing hooks
# -------------------------------------------------------------------------
def pytest_addhooks(pluginmanager):
from xdist import newhooks
pluginmanager.add_hookspecs(newhooks)
# -------------------------------------------------------------------------
# distributed testing initialization
# -------------------------------------------------------------------------
@pytest.mark.trylast
def pytest_configure(config):
if config.getoption("dist") != "no" and not config.getvalue("collectonly"):
from xdist.dsession import DSession
session = DSession(config)
config.pluginmanager.register(session, "dsession")
tr = config.pluginmanager.getplugin("terminalreporter")
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"
numprocesses = config.option.numprocesses
if config.option.maxprocesses:
numprocesses = min(numprocesses, config.option.maxprocesses)
config.option.tx = ["popen"] * numprocesses
if config.option.distload:
config.option.dist = "load"
val = config.getvalue
if not val("collectonly"):
if val("dist") != "no":
if usepdb:
raise pytest.UsageError(
"--pdb is incompatible with distributing tests; try using -n0 or -nauto."
) # noqa: E501
# -------------------------------------------------------------------------
# fixtures
# -------------------------------------------------------------------------
@pytest.fixture(scope="session")
def worker_id(request):
"""Return the id of the current worker ('gw0', 'gw1', etc) or 'master'
if running on the master node.
"""
if hasattr(request.config, "workerinput"):
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

261
src/xdist/remote.py Normal file
View File

@@ -0,0 +1,261 @@
"""
This module is executed in remote subprocesses and helps to
control a remote testing session and relay back information.
It assumes that 'py' is importable and does not have dependencies
on the rest of the xdist code. This means that the xdist-plugin
needs not to be installed in remote environments.
"""
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)
self.channel = channel
config.pluginmanager.register(self)
def sendevent(self, name, **kwargs):
self.log("sending", name, kwargs)
self.channel.send((name, kwargs))
def pytest_internalerror(self, excrepr):
for line in str(excrepr).split("\n"):
self.log("IERROR>", line)
def pytest_sessionstart(self, session):
self.session = session
workerinfo = getinfodict()
self.sendevent("workerready", workerinfo=workerinfo)
@pytest.hookimpl(hookwrapper=True)
def pytest_sessionfinish(self, exitstatus):
# in pytest 5.0+, exitstatus is an IntEnum object
self.config.workeroutput["exitstatus"] = int(exitstatus)
yield
self.sendevent("workerfinished", workeroutput=self.config.workeroutput)
def pytest_collection(self, session):
self.sendevent("collectionstart")
def pytest_runtestloop(self, session):
self.log("entering main loop")
torun = []
while 1:
try:
name, kwargs = self.channel.receive()
except EOFError:
return True
self.log("received command", name, kwargs)
if name == "runtests":
torun.extend(kwargs["indices"])
elif name == "runtests_all":
torun.extend(range(len(session.items)))
self.log("items to run:", torun)
# only run if we have an item and a next item
while len(torun) >= 2:
self.run_one_test(torun)
if name == "shutdown":
if torun:
self.run_one_test(torun)
break
return True
def run_one_test(self, torun):
items = self.session.items
self.item_index = torun.pop(0)
item = items[self.item_index]
if torun:
nextitem = items[torun[0]]
else:
nextitem = None
start = time.time()
self.config.hook.pytest_runtest_protocol(item=item, nextitem=nextitem)
duration = time.time() - start
self.sendevent(
"runtest_protocol_complete", item_index=self.item_index, duration=duration
)
def pytest_collection_finish(self, session):
self.sendevent(
"collectionfinish",
topdir=str(session.fspath),
ids=[item.nodeid for item in session.items],
)
def pytest_runtest_logstart(self, nodeid, location):
self.sendevent("logstart", nodeid=nodeid, location=location)
# the pytest_runtest_logfinish hook was introduced in pytest 3.4
if hasattr(_pytest.hookspec, "pytest_runtest_logfinish"):
def pytest_runtest_logfinish(self, nodeid, location):
self.sendevent("logfinish", nodeid=nodeid, location=location)
def pytest_runtest_logreport(self, 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):
# 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)
# 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"):
def pytest_warning_captured(self, warning_message, when, item):
self.sendevent(
"warning_captured",
warning_message_data=serialize_warning_message(warning_message),
when=when,
# item cannot be serialized and will always be None when used with xdist
item=None,
)
def serialize_warning_message(warning_message):
if isinstance(warning_message.message, Warning):
message_module = type(warning_message.message).__module__
message_class_name = type(warning_message.message).__name__
message_str = str(warning_message.message)
# check now if we can serialize the warning arguments (#349)
# if not, we will just use the exception message on the master node
try:
dumps(warning_message.message.args)
except DumpError:
message_args = None
else:
message_args = warning_message.message.args
else:
message_str = warning_message.message
message_module = None
message_class_name = None
message_args = None
if warning_message.category:
category_module = warning_message.category.__module__
category_class_name = warning_message.category.__name__
else:
category_module = None
category_class_name = None
result = {
"message_str": message_str,
"message_module": message_module,
"message_class_name": message_class_name,
"message_args": message_args,
"category_module": category_module,
"category_class_name": category_class_name,
}
# access private _WARNING_DETAILS because the attributes vary between Python versions
for attr_name in warning_message._WARNING_DETAILS:
if attr_name in ("message", "category"):
continue
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
def getinfodict():
import platform
return dict(
version=sys.version,
version_info=tuple(sys.version_info),
sysplatform=sys.platform,
platform=platform.platform(),
executable=sys.executable,
cwd=os.getcwd(),
)
def remote_initconfig(option_dict, args):
option_dict["plugins"].append("no:terminal")
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.option.basetemp = basetemp
if __name__ == "__channelexec__":
channel = channel # noqa
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"])
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
config.slaveinput = config.workerinput
config.slaveoutput = config.workeroutput
interactor = WorkerInteractor(config, channel)
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,4 +1,4 @@
from xdist.scheduler.each import EachScheduling # noqa from xdist.scheduler.each import EachScheduling # noqa
from xdist.scheduler.load import LoadScheduling # noqa from xdist.scheduler.load import LoadScheduling # noqa
from xdist.scheduler.loadfile import LoadFileScheduling # noqa
from xdist.scheduler.loadscope import LoadScopeScheduling # noqa from xdist.scheduler.loadscope import LoadScopeScheduling # noqa
from xdist.scheduler.filescope import LoadFileScheduling # noqa

View File

@@ -4,7 +4,7 @@ from xdist.workermanage import parse_spec_config
from xdist.report import report_collection_diff from xdist.report import report_collection_diff
class EachScheduling: 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(
dead_collection,
collection, collection,
deadnode.gateway.id, deadnode.gateway.id,
node.gateway.id) node.gateway.id,
)
self.log(msg) self.log(msg)
return return
pending = self._removed2pending.pop(deadnode) pending = self._removed2pending.pop(deadnode)
@@ -124,6 +126,7 @@ class EachScheduling:
if not pending: if not pending:
pending[:] = range(len(self.node2collection[node])) pending[:] = range(len(self.node2collection[node]))
node.send_runtest_all() node.send_runtest_all()
node.shutdown()
else: else:
node.send_runtest_some(pending) node.send_runtest_some(pending)
self._started.append(node) self._started.append(node)

View File

@@ -7,7 +7,7 @@ from xdist.workermanage import parse_spec_config
from xdist.report import report_collection_diff from xdist.report import report_collection_diff
class LoadScheduling: 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
@@ -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)
@@ -179,6 +178,9 @@ class LoadScheduling:
return return
num_send = items_per_node_max - len(node_pending) num_send = items_per_node_max - len(node_pending)
self._send_tests(node, num_send) self._send_tests(node, num_send)
else:
node.shutdown()
self.log("num items waiting for node:", len(self.pending)) self.log("num items waiting for node:", len(self.pending))
def remove_node(self, node): def remove_node(self, node):
@@ -226,7 +228,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 +240,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 +272,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

@@ -1,4 +1,4 @@
from . import LoadScopeScheduling from .loadscope import LoadScopeScheduling
from py.log import Producer from py.log import Producer
@@ -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

@@ -6,7 +6,7 @@ from xdist.report import report_collection_diff
from xdist.workermanage import parse_spec_config from xdist.workermanage import parse_spec_config
class LoadScopeScheduling: 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
@@ -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
@@ -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
@@ -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):
@@ -315,9 +306,10 @@ class LoadScopeScheduling:
# Check that more work is available # Check that more work is available
if not self.workqueue: if not self.workqueue:
node.shutdown()
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
@@ -348,13 +340,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,12 +358,12 @@ 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 {} nodes".format(extra_nodes))
for _ in range(extra_nodes): for _ in range(extra_nodes):
unused_node, assigned = self.assigned_work.popitem(last=True) unused_node, assigned = self.assigned_work.popitem(last=True)
self.log('Shuting down unused node {0}'.format(unused_node)) self.log("Shuting down unused node {}".format(unused_node))
unused_node.shutdown() unused_node.shutdown()
# Assign initial workload # Assign initial workload
@@ -402,10 +392,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
@@ -416,12 +403,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,13 +1,15 @@
from __future__ import print_function
import fnmatch import fnmatch
import os import os
import re import re
import sys
import uuid
import py import py
import pytest import pytest
import execnet import execnet
import xdist.remote
from _pytest import runner # XXX load dynamically import xdist.remote
def parse_spec_config(config): def parse_spec_config(config):
@@ -22,18 +24,21 @@ def parse_spec_config(config):
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.trace = self.config.trace.get("nodemanager") self.trace = self.config.trace.get("nodemanager")
self.testrunuid = self.config.getoption("testrunuid")
if self.testrunuid is None:
self.testrunuid = uuid.uuid4().hex
self.group = execnet.Group() self.group = execnet.Group()
if specs is None: if specs is None:
specs = self._getxspecs() specs = self._getxspecs()
@@ -47,7 +52,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 +61,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:
@@ -88,6 +92,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
@@ -100,7 +105,7 @@ class NodeManager(object):
for root in candidates: for root in candidates:
root = py.path.local(root).realpath() root = py.path.local(root).realpath()
if not root.check(): if not root.check():
raise pytest.UsageError("rsyncdir doesn't exist: %r" % (root,)) raise pytest.UsageError("rsyncdir doesn't exist: {!r}".format(root))
if root not in roots: if root not in roots:
roots.append(root) roots.append(root)
return roots return roots
@@ -112,8 +117,8 @@ class NodeManager(object):
ignores += self.config.getini("rsyncignore") ignores += self.config.getini("rsyncignore")
return { return {
'ignores': ignores, "ignores": ignores,
'verbose': self.config.option.verbose, "verbose": getattr(self.config.option, "verbose", False),
} }
def rsync(self, gateway, source, notify=None, verbose=False, ignores=None): def rsync(self, gateway, source, notify=None, verbose=False, ignores=None):
@@ -126,9 +131,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 +144,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,32 +175,34 @@ 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("{}:{} <= {}".format(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:
parts = arg.split(splitcode) parts = arg.split(splitcode)
fspath = py.path.local(parts[0]) fspath = py.path.local(parts[0])
if not fspath.exists():
result.append(arg)
continue
for root in roots: for root in roots:
x = fspath.relto(root) x = fspath.relto(root)
if x or fspath == root: if x or fspath == root:
parts[0] = root.basename + "/" + x parts[0] = root.basename + "/" + x
break break
else: else:
raise ValueError("arg %s not relative to an rsync root" % (arg,)) raise ValueError("arg {} not relative to an rsync root".format(arg))
result.append(splitcode.join(parts)) result.append(splitcode.join(parts))
return result return result
@@ -204,15 +210,24 @@ def make_reltoroot(roots, args):
class WorkerController(object): class WorkerController(object):
ENDMARK = -1 ENDMARK = -1
class RemoteHook:
@pytest.mark.trylast
def pytest_xdist_getremotemodule(self):
return xdist.remote
def __init__(self, nodemanager, gateway, config, putevent): def __init__(self, nodemanager, gateway, config, putevent):
config.pluginmanager.register(self.RemoteHook())
self.nodemanager = nodemanager self.nodemanager = nodemanager
self.putevent = putevent self.putevent = putevent
self.gateway = gateway self.gateway = gateway
self.config = config self.config = config
self.workerinput = {'workerid': gateway.id, self.workerinput = {
'workercount': len(nodemanager.specs), "workerid": gateway.id,
'slaveid': gateway.id, "workercount": len(nodemanager.specs),
'slavecount': 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 # TODO: deprecated name, backward compatibility only. Remove it in future
self.slaveinput = self.workerinput self.slaveinput = self.workerinput
@@ -223,7 +238,7 @@ class WorkerController(object):
py.log.setconsumer(self.log._keywords, None) py.log.setconsumer(self.log._keywords, None)
def __repr__(self): def __repr__(self):
return "<%s %s>" % (self.__class__.__name__, self.gateway.id,) return "<{} {}>".format(self.__class__.__name__, self.gateway.id)
@property @property
def shutting_down(self): def shutting_down(self):
@@ -232,30 +247,37 @@ class WorkerController(object):
def setup(self): def setup(self):
self.log("setting up worker session") self.log("setting up worker session")
spec = self.gateway.spec spec = self.gateway.spec
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 args = self.config.args
option_dict = vars(self.config.option)
if not spec.popen or spec.chdir: if not spec.popen or spec.chdir:
args = make_reltoroot(self.nodemanager.roots, args) args = make_reltoroot(self.nodemanager.roots, args)
option_dict = vars(self.config.option)
if spec.popen: if spec.popen:
name = "popen-%s" % self.gateway.id name = "popen-%s" % self.gateway.id
if hasattr(self.config, '_tmpdirhandler'): if hasattr(self.config, "_tmpdirhandler"):
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.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: 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
@@ -264,23 +286,23 @@ class WorkerController(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:
try: try:
self.sendcommand("shutdown") self.sendcommand("shutdown")
except IOError: except (IOError, OSError):
pass pass
self._shutdown_sent = True self._shutdown_sent = True
def sendcommand(self, name, **kwargs): def sendcommand(self, name, **kwargs):
""" send a named parametrized command to the other side. """ """ send a named parametrized command to the other side. """
self.log("sending command %s(**%s)" % (name, kwargs)) self.log("sending command {}(**{})".format(name, kwargs))
self.channel.send((name, kwargs)) self.channel.send((name, kwargs))
def notify_inproc(self, eventname, **kwargs): def notify_inproc(self, eventname, **kwargs):
self.log("queuing %s(**%s)" % (eventname, kwargs)) self.log("queuing {}(**{})".format(eventname, kwargs))
self.putevent((eventname, kwargs)) self.putevent((eventname, kwargs))
def process_from_remote(self, eventcall): # noqa too complex def process_from_remote(self, eventcall): # noqa too complex
@@ -302,119 +324,101 @@ class WorkerController(object):
return return
eventname, kwargs = eventcall eventname, kwargs = eventcall
if eventname in ("collectionstart",): if eventname in ("collectionstart",):
self.log("ignoring %s(%s)" % (eventname, kwargs)) self.log("ignoring {}({})".format(eventname, kwargs))
elif eventname == "workerready": elif eventname == "workerready":
self.notify_inproc(eventname, node=self, **kwargs) self.notify_inproc(eventname, node=self, **kwargs)
elif eventname == "workerfinished": elif eventname == "workerfinished":
self._down = True self._down = True
self.workeroutput = kwargs['workeroutput'] self.workeroutput = self.slaveoutput = kwargs["workeroutput"]
self.notify_inproc("workerfinished", node=self) self.notify_inproc("workerfinished", node=self)
elif eventname in ("logstart", "logfinish"): elif eventname in ("logstart", "logfinish"):
self.notify_inproc(eventname, node=self, **kwargs) self.notify_inproc(eventname, node=self, **kwargs)
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 = self.config.hook.pytest_report_from_serializable(
config=self.config, data=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"],
)
elif eventname == "warning_captured":
warning_message = unserialize_warning_message(
kwargs["warning_message_data"]
)
self.notify_inproc(
eventname,
warning_message=warning_message,
when=kwargs["when"],
item=kwargs["item"],
)
else: else:
raise ValueError("unknown event: %s" % (eventname,)) raise ValueError("unknown event: {}".format(eventname))
except KeyboardInterrupt: except KeyboardInterrupt:
# should not land in receiver-thread # should not land in receiver-thread
raise raise
except: # noqa except: # noqa
excinfo = py.code.ExceptionInfo() from _pytest._code import ExceptionInfo
py.builtin.print_("!" * 20, excinfo)
# 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.config.notify_exception(excinfo)
self.shutdown() self.shutdown()
self.notify_inproc("errordown", node=self, error=excinfo) self.notify_inproc("errordown", node=self, error=excinfo)
def unserialize_report(name, reportdict): def unserialize_warning_message(data):
def assembled_report(reportdict): import warnings
from _pytest._code.code import ( import importlib
ReprEntry,
ReprEntryNative, if data["message_module"]:
ReprExceptionInfo, mod = importlib.import_module(data["message_module"])
ReprFileLocation, cls = getattr(mod, data["message_class_name"])
ReprFuncArgs, message = None
ReprLocals, if data["message_args"] is not None:
ReprTraceback try:
message = cls(*data["message_args"])
except TypeError:
pass
if message is None:
# could not recreate the original warning instance;
# create a generic Warning instance with the original
# message at least
message_text = "{mod}.{cls}: {msg}".format(
mod=data["message_module"],
cls=data["message_class_name"],
msg=data["message_str"],
) )
if reportdict['longrepr']: message = Warning(message_text)
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: else:
report_unserialization_failure( message = data["message_str"]
entry_type, name, reportdict)
unserialized_entries.append(reprentry)
reprtraceback['reprentries'] = unserialized_entries
exception_info = ReprExceptionInfo( if data["category_module"]:
reprtraceback=ReprTraceback(**reprtraceback), mod = importlib.import_module(data["category_module"])
reprcrash=ReprFileLocation(**reprcrash), category = getattr(mod, data["category_class_name"])
) else:
category = None
for section in reportdict['longrepr']['sections']: kwargs = {"message": message, "category": category}
exception_info.addsection(*section) # access private _WARNING_DETAILS because the attributes vary between Python versions
reportdict['longrepr'] = exception_info for attr_name in warnings.WarningMessage._WARNING_DETAILS:
return reportdict if attr_name in ("message", "category"):
continue
kwargs[attr_name] = data[attr_name]
if name == "testreport": return warnings.WarningMessage(**kwargs)
return runner.TestReport(**assembled_report(reportdict))
elif name == "collectreport":
return runner.CollectReport(**assembled_report(reportdict))
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()

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,11 @@
import six
import py import py
import pytest import pytest
import execnet import execnet
pytest_plugins = "pytester"
if six.PY2:
@pytest.fixture(scope="session", autouse=True) @pytest.fixture(scope="session", autouse=True)
def _ensure_imports(): def _ensure_imports():
@@ -12,14 +16,10 @@ def _ensure_imports():
execnet.makegateway execnet.makegateway
pytest_plugins = "pytester"
# rsyncdirs = ['.', '../xdist', py.path.local(execnet.__file__).dirpath()]
@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 +31,12 @@ def _divert_atexit(request, monkeypatch):
def pytest_addoption(parser): def pytest_addoption(parser):
parser.addoption('--gx', parser.addoption(
"--gx",
action="append", action="append",
dest="gspecs", dest="gspecs",
help="add a global test environment, XSpec-syntax. ") help="add a global test environment, XSpec-syntax. ",
)
@pytest.fixture @pytest.fixture
@@ -42,14 +44,6 @@ def specssh(request):
return getspecssh(request.config) return getspecssh(request.config)
@pytest.fixture
def testdir(testdir):
# pytest before 2.8 did not have a runpytest_subprocess
if not hasattr(testdir, "runpytest_subprocess"):
testdir.runpytest_subprocess = testdir.runpytest
return testdir
# configuration information for tests # configuration information for tests
def getgspecs(config): def getgspecs(config):
return [execnet.XSpec(spec) for spec in config.getvalueorskip("gspecs")] return [execnet.XSpec(spec) for spec in config.getvalueorskip("gspecs")]

View File

@@ -1,9 +1,6 @@
from xdist.dsession import DSession from xdist.dsession import DSession, get_default_max_worker_restart
from xdist.report import report_collection_diff from xdist.report import report_collection_diff
from xdist.scheduler import ( 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
@@ -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,68 @@ 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_default_max_worker_restart():
class config:
class option:
maxworkerrestart = None
numprocesses = 0
assert get_default_max_worker_restart(config) is None
config.option.numprocesses = 2
assert get_default_max_worker_restart(config) == 8
config.option.maxworkerrestart = "1"
assert get_default_max_worker_restart(config) == 1
config.option.maxworkerrestart = "0"
assert get_default_max_worker_restart(config) == 0
def test_report_collection_diff_different(): def test_report_collection_diff_different():
"""Test reporting of different collections.""" """Test reporting of different collections."""
from_collection = ['aaa', 'bbb', 'ccc', 'YYY'] from_collection = ["aaa", "bbb", "ccc", "YYY"]
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

@@ -1,4 +1,7 @@
import py import py
import pytest
from pkg_resources import parse_version
from xdist.looponfail import RemoteControl from xdist.looponfail import RemoteControl
from xdist.looponfail import StatRecorder from xdist.looponfail import StatRecorder
@@ -60,7 +63,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 +87,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 +113,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 +140,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 +160,91 @@ 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", @pytest.mark.xfail(
reason="broken by pytest 3.1+") parse_version(pytest.__version__) >= parse_version("3.1"),
reason="broken by pytest 3.1+",
strict=True,
)
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 +253,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 +268,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 +290,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,62 @@ 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", "worker_id", and "testrun_uid" 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":
workerid = report.node.workerinput['workerid'] workerid = report.node.workerinput['workerid']
testrunuid = report.node.workerinput['testrunuid']
if workerid != report.worker_id: if workerid != report.worker_id:
print("HOOK: Worker id mismatch: %s %s" print("HOOK: Worker id mismatch: %s %s"
% (workerid, report.worker_id)) % (workerid, report.worker_id))
elif testrunuid != report.testrun_uid:
print("HOOK: Testrun uid mismatch: %s %s"
% (testrunuid, report.testrun_uid))
else: else:
print("HOOK: %s %s" print("HOOK: %s %s %s"
% (report.nodeid, report.worker_id)) % (report.nodeid, report.worker_id, report.testrun_uid))
""") """
res = testdir.runpytest('-n1', '-s') )
res.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):
workerid = node.workerinput['workerid'] 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" % (workerid, ', '.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

@@ -16,14 +16,19 @@ 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("--numprocesses", "3", "--maxprocesses", "2")
check_options(config)
assert config.option.dist == "load"
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 +36,37 @@ 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'): from xdist.plugin import pytest_cmdline_main as check_options
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
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")
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
@@ -74,6 +95,18 @@ def test_dsession_with_collect_only(testdir):
assert not config.pluginmanager.hasplugin("dsession") assert not config.pluginmanager.hasplugin("dsession")
def test_testrunuid_provided(testdir):
config = testdir.parseconfigure("--testrunuid", "test123", "--tx=popen")
nm = NodeManager(config)
assert nm.testrunuid == "test123"
def test_testrunuid_generated(testdir):
config = testdir.parseconfigure("--tx=popen")
nm = NodeManager(config)
assert len(nm.testrunuid) == 32
class TestDistOptions: class TestDistOptions:
def test_getxspecs(self, testdir): def test_getxspecs(self, testdir):
config = testdir.parseconfigure("--tx=popen", "--tx", "ssh=xyz") config = testdir.parseconfigure("--tx=popen", "--tx", "ssh=xyz")
@@ -85,13 +118,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 +132,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,14 @@
import py import py
import pprint
import pytest import pytest
from xdist.workermanage import WorkerController, unserialize_report import sys
from xdist.remote import serialize_report import uuid
from xdist.workermanage import WorkerController
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 +17,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")
@@ -23,7 +26,7 @@ class EventCall:
self.name, self.kwargs = eventcall self.name, self.kwargs = eventcall
def __str__(self): def __str__(self):
return "<EventCall %s(**%s)>" % (self.name, self.kwargs) return "<EventCall {}(**{})>".format(self.name, self.kwargs)
class WorkerSetup: class WorkerSetup:
@@ -32,7 +35,7 @@ class WorkerSetup:
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()
@@ -42,10 +45,10 @@ class WorkerSetup:
putevent = self.use_callback and self.events.put or None putevent = self.use_callback and self.events.put or None
class DummyMananger: class DummyMananger:
testrunuid = uuid.uuid4().hex
specs = [0, 1] specs = [0, 1]
self.slp = WorkerController(DummyMananger, self.gateway, config, 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,7 +61,7 @@ class WorkerSetup:
ev = EventCall(data) ev = EventCall(data)
if name is None or ev.name == name: if name is None or ev.name == name:
return ev return ev
print("skipping %s" % (ev, )) print("skipping {}".format(ev))
def sendcommand(self, name, **kwargs): def sendcommand(self, name, **kwargs):
self.slp.sendcommand(name, **kwargs) self.slp.sendcommand(name, **kwargs)
@@ -69,186 +72,33 @@ def worker(request, testdir):
return WorkerSetup(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__
assert config2.pluginmanager.getplugin("terminal") in (-1, None) 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: class TestWorkerInteractor:
def test_basic_collect_and_runtests(self, worker): @pytest.fixture
worker.testdir.makepyfile(""" 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(): def test_func():
pass pass
""") """
)
worker.setup() worker.setup()
ev = worker.popevent() ev = worker.popevent()
assert ev.name == "workerready" assert ev.name == "workerready"
@@ -256,8 +106,8 @@ class TestWorkerInteractor:
assert ev.name == "collectionstart" assert ev.name == "collectionstart"
assert not ev.kwargs assert not ev.kwargs
ev = worker.popevent("collectionfinish") ev = worker.popevent("collectionfinish")
assert ev.kwargs['topdir'] == worker.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
worker.sendcommand("runtests", indices=list(range(len(ids)))) worker.sendcommand("runtests", indices=list(range(len(ids))))
worker.sendcommand("shutdown") worker.sendcommand("shutdown")
@@ -267,51 +117,50 @@ class TestWorkerInteractor:
ev = worker.popevent("testreport") # setup ev = worker.popevent("testreport") # setup
ev = worker.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.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 = worker.popevent("workerfinished") ev = worker.popevent("workerfinished")
assert 'workeroutput' in ev.kwargs assert "workeroutput" in ev.kwargs
@pytest.mark.skipif(pytest.__version__ >= '3.0', def test_remote_collect_skip(self, worker, unserialize_report):
reason='skip at module level illegal in pytest 3.0') worker.testdir.makepyfile(
def test_remote_collect_skip(self, worker): """
worker.testdir.makepyfile(""" import pytest
import py pytest.skip("hello", allow_module_level=True)
py.test.skip("hello") """
""") )
worker.setup() worker.setup()
ev = worker.popevent("collectionstart") ev = worker.popevent("collectionstart")
assert not ev.kwargs assert not ev.kwargs
ev = worker.popevent() ev = worker.popevent()
assert ev.name == "collectreport" assert ev.name == "collectreport"
ev = worker.popevent() rep = unserialize_report(ev.kwargs["data"])
assert ev.name == "collectreport"
rep = unserialize_report(ev.name, ev.kwargs['data'])
assert rep.skipped assert rep.skipped
assert rep.longrepr[2] == "Skipped: hello"
ev = worker.popevent("collectionfinish") ev = worker.popevent("collectionfinish")
assert not ev.kwargs['ids'] 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.testdir.makepyfile("""aasd qwe""")
worker.setup() worker.setup()
ev = worker.popevent("collectionstart") ev = worker.popevent("collectionstart")
assert not ev.kwargs assert not ev.kwargs
ev = worker.popevent() ev = worker.popevent()
assert ev.name == "collectreport" assert ev.name == "collectreport"
ev = worker.popevent() rep = unserialize_report(ev.kwargs["data"])
assert ev.name == "collectreport"
rep = unserialize_report(ev.name, ev.kwargs['data'])
assert rep.failed assert rep.failed
ev = worker.popevent("collectionfinish") ev = worker.popevent("collectionfinish")
assert not ev.kwargs['ids'] assert not ev.kwargs["ids"]
def test_runtests_all(self, worker): def test_runtests_all(self, worker, unserialize_report):
worker.testdir.makepyfile(""" worker.testdir.makepyfile(
"""
def test_func(): pass def test_func(): pass
def test_func2(): pass def test_func2(): pass
""") """
)
worker.setup() worker.setup()
ev = worker.popevent() ev = worker.popevent()
assert ev.name == "workerready" assert ev.name == "workerready"
@@ -319,57 +168,125 @@ class TestWorkerInteractor:
assert ev.name == "collectionstart" assert ev.name == "collectionstart"
assert not ev.kwargs assert not ev.kwargs
ev = worker.popevent("collectionfinish") ev = worker.popevent("collectionfinish")
ids = ev.kwargs['ids'] ids = ev.kwargs["ids"]
assert len(ids) == 2 assert len(ids) == 2
worker.sendcommand("runtests_all", ) worker.sendcommand("runtests_all")
worker.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 = worker.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.kwargs["data"])
assert rep.nodeid.endswith(func) assert rep.nodeid.endswith(func)
ev = worker.popevent("workerfinished") ev = worker.popevent("workerfinished")
assert 'workeroutput' in ev.kwargs assert "workeroutput" in ev.kwargs
def test_happy_run_events_converted(self, testdir, worker): 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 worker.use_callback assert not worker.use_callback
worker.testdir.makepyfile(""" worker.testdir.makepyfile(
"""
def test_func(): def test_func():
pass pass
""") """
)
worker.setup() worker.setup()
hookrec = testdir.getreportrecorder(worker.config) hookrec = testdir.getreportrecorder(worker.config)
for data in worker.slp.channel: for data in worker.slp.channel:
worker.slp.process_from_remote(data) worker.slp.process_from_remote(data)
worker.slp.process_from_remote(worker.slp.ENDMARK) worker.slp.process_from_remote(worker.slp.ENDMARK)
py.std.pprint.pprint(hookrec.hookrecorder.calls) pprint.pprint(hookrec.hookrecorder.calls)
hookrec.hookrecorder.contains([ hookrec.hookrecorder.contains(
[
("pytest_collectstart", "collector.fspath == aaa"), ("pytest_collectstart", "collector.fspath == aaa"),
("pytest_pycollect_makeitem", "name == 'test_func'"), ("pytest_pycollect_makeitem", "name == 'test_func'"),
("pytest_collectreport", "report.collector.fspath == aaa"), ("pytest_collectreport", "report.collector.fspath == aaa"),
("pytest_collectstart", "collector.fspath == bbb"), ("pytest_collectstart", "collector.fspath == bbb"),
("pytest_pycollect_makeitem", "name == 'test_func'"), ("pytest_pycollect_makeitem", "name == 'test_func'"),
("pytest_collectreport", "report.collector.fspath == bbb"), ("pytest_collectreport", "report.collector.fspath == bbb"),
]) ]
)
def test_process_from_remote_error_handling(self, worker, capsys): def test_process_from_remote_error_handling(self, worker, capsys):
worker.use_callback = True worker.use_callback = True
worker.setup() worker.setup()
worker.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 = worker.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 len(os.environ['PYTEST_XDIST_TESTRUNUID']) == 32
assert os.environ['PYTEST_XDIST_WORKER'] in ('gw0', 'gw1') assert os.environ['PYTEST_XDIST_WORKER'] in ('gw0', 'gw1')
assert os.environ['PYTEST_XDIST_WORKER_COUNT'] == '2' assert os.environ['PYTEST_XDIST_WORKER_COUNT'] == '2'
''') """
result = testdir.runpytest('-n2', '--max-worker-restart=0') )
result = testdir.runpytest("-n2", "--max-worker-restart=0")
assert result.ret == 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

@@ -1,5 +1,7 @@
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 workermanage, newhooks from xdist import workermanage, newhooks
@@ -40,7 +42,7 @@ def workercontroller(monkeypatch):
def setup(self): def setup(self):
pass pass
monkeypatch.setattr(workermanage, 'WorkerController', 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):
workercontroller):
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")
@@ -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(
workercontroller): 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
@@ -166,10 +168,11 @@ class TestNodeManager:
# assert nodemanager.config.topdir == source == config.topdir # assert nodemanager.config.topdir == source == config.topdir
nodemanager.makegateways() nodemanager.makegateways()
nodemanager.rsync_roots() nodemanager.rsync_roots()
p, = nodemanager.gwmanager.multi_exec( (p,) = nodemanager.gwmanager.multi_exec(
"import os ; channel.send(os.getcwd())").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()
@@ -181,15 +184,17 @@ class TestNodeManager:
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, workercontroller): def test_init_rsync_roots(self, testdir, mysetup, workercontroller):
@@ -198,10 +203,14 @@ class TestNodeManager:
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()
@@ -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, workercontroller): 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(
__init__="",
test_x=""" 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(
rep, = reprec.getreports("pytest_runtest_logreport") "-d", "--rsyncdir=%s" % testdir.tmpdir, "--tx", specssh, testdir.tmpdir
)
(rep,) = reprec.getreports("pytest_runtest_logreport")
assert rep.passed assert rep.passed

63
tox.ini
View File

@@ -1,46 +1,41 @@
[tox] [tox]
# 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,35,36,37,38}-pytestlatest
py{27,36}-pytest{master,features} py38-pytestmaster
flakes
readme
[testenv] [testenv]
changedir=testing
passenv = USER USERNAME passenv = USER USERNAME
extras = testing
deps = deps =
pycmd pytestlatest: pytest
# 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
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
pexpect: pexpect
platform=
pexpect: linux|darwin
commands= commands=
# always clean to avoid code unmarshal mismatch on old python/pytest pytest {posargs}
py.cleanup -aq
py.test {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.7
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 = -ra
;; hello testpaths = testing
[flake8]
max-line-length = 120
ignore = E203,W503

View File

@@ -1,138 +0,0 @@
import py
import pytest
def parse_numprocesses(s):
if s == 'auto':
try:
from os import cpu_count
except ImportError:
from multiprocessing import cpu_count
try:
n = cpu_count()
except NotImplementedError:
return 1
return n if n else 1
else:
return int(s)
def pytest_addoption(parser):
group = parser.getgroup("xdist", "distributed and subprocess testing")
group._addoption(
'-n', '--numprocesses', dest="numprocesses", metavar="numprocesses",
action="store",
type=parse_numprocesses,
help="shortcut for '--dist=load --tx=NUM*popen', "
"you can use 'auto' here for auto detection CPUs number on "
"host system")
group.addoption('--max-worker-restart', '--max-slave-restart', action="store", default=None,
dest="maxworkerrestart",
help="maximum number of workers that can be restarted "
"when crashed (set to zero to disable this feature)\n"
"'--max-slave-restart' option is deprecated and will be removed in "
"a future release")
group.addoption(
'--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",
help=("add a test execution environment. some examples: "
"--tx popen//python=python2.5 --tx socket=192.168.1.102:8888 "
"--tx ssh=user@codespeak.net//chdir=testcache"))
group._addoption(
'-d',
action="store_true", dest="distload", default=False,
help="load-balance tests. shortcut for '--dist=load'")
group.addoption(
'--rsyncdir', action="append", default=[], metavar="DIR",
help="add directory for rsyncing to remote tx nodes.")
group.addoption(
'--rsyncignore', action="append", default=[], 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")
parser.addini(
'rsyncdirs', '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.', type="pathlist")
parser.addini(
"looponfailroots", type="pathlist",
help="directories to check for changes", default=[py.path.local()])
# -------------------------------------------------------------------------
# distributed testing hooks
# -------------------------------------------------------------------------
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)
# -------------------------------------------------------------------------
# distributed testing initialization
# -------------------------------------------------------------------------
@pytest.mark.trylast
def pytest_configure(config):
if config.getoption("dist") != "no" and not config.getvalue("collectonly"):
from xdist.dsession import DSession
session = DSession(config)
config.pluginmanager.register(session, "dsession")
tr = config.pluginmanager.getplugin("terminalreporter")
tr.showfspath = False
if config.getoption("boxed"):
config.option.forked = True
@pytest.mark.tryfirst
def pytest_cmdline_main(config):
if config.option.numprocesses:
if config.option.dist == 'no':
config.option.dist = "load"
config.option.tx = ['popen'] * config.option.numprocesses
if config.option.distload:
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.") # noqa: E501
# -------------------------------------------------------------------------
# fixtures
# -------------------------------------------------------------------------
@pytest.fixture(scope="session")
def worker_id(request):
"""Return the id of the current worker ('gw0', 'gw1', etc) or 'master'
if running on the master node.
"""
if hasattr(request.config, 'workerinput'):
return request.config.workerinput['workerid']
else:
return 'master'

View File

@@ -1,205 +0,0 @@
"""
This module is executed in remote subprocesses and helps to
control a remote testing session and relay back information.
It assumes that 'py' is importable and does not have dependencies
on the rest of the xdist code. This means that the xdist-plugin
needs not to be installed in remote environments.
"""
import sys
import os
import time
import _pytest.hookspec
import pytest
class WorkerInteractor:
def __init__(self, config, channel):
self.config = config
self.workerid = config.workerinput.get('workerid', "?")
self.log = py.log.Producer("worker-%s" % self.workerid)
if not config.option.debug:
py.log.setconsumer(self.log._keywords, None)
self.channel = channel
config.pluginmanager.register(self)
def sendevent(self, name, **kwargs):
self.log("sending", name, kwargs)
self.channel.send((name, kwargs))
def pytest_internalerror(self, excrepr):
for line in str(excrepr).split("\n"):
self.log("IERROR>", line)
def pytest_sessionstart(self, session):
self.session = session
workerinfo = getinfodict()
self.sendevent("workerready", workerinfo=workerinfo)
@pytest.hookimpl(hookwrapper=True)
def pytest_sessionfinish(self, exitstatus):
self.config.workeroutput['exitstatus'] = exitstatus
yield
self.sendevent("workerfinished", workeroutput=self.config.workeroutput)
def pytest_collection(self, session):
self.sendevent("collectionstart")
def pytest_runtestloop(self, session):
self.log("entering main loop")
torun = []
while 1:
try:
name, kwargs = self.channel.receive()
except EOFError:
return True
self.log("received command", name, kwargs)
if name == "runtests":
torun.extend(kwargs['indices'])
elif name == "runtests_all":
torun.extend(range(len(session.items)))
self.log("items to run:", torun)
# only run if we have an item and a next item
while len(torun) >= 2:
self.run_one_test(torun)
if name == "shutdown":
if torun:
self.run_one_test(torun)
break
return True
def run_one_test(self, torun):
items = self.session.items
self.item_index = torun.pop(0)
item = items[self.item_index]
if torun:
nextitem = items[torun[0]]
else:
nextitem = None
start = time.time()
self.config.hook.pytest_runtest_protocol(
item=item,
nextitem=nextitem)
duration = time.time() - start
self.sendevent("runtest_protocol_complete", item_index=self.item_index,
duration=duration)
def pytest_collection_finish(self, session):
self.sendevent(
"collectionfinish",
topdir=str(session.fspath),
ids=[item.nodeid for item in session.items])
def pytest_runtest_logstart(self, nodeid, location):
self.sendevent("logstart", nodeid=nodeid, location=location)
# the pytest_runtest_logfinish hook was introduced in pytest 3.4
if hasattr(_pytest.hookspec, 'pytest_runtest_logfinish'):
def pytest_runtest_logfinish(self, nodeid, location):
self.sendevent("logfinish", nodeid=nodeid, location=location)
def pytest_runtest_logreport(self, report):
data = serialize_report(report)
data["item_index"] = self.item_index
data["worker_id"] = self.workerid
assert self.session.items[self.item_index].nodeid == report.nodeid
self.sendevent("testreport", data=data)
def pytest_collectreport(self, report):
data = serialize_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))
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 getinfodict():
import platform
return dict(
version=sys.version,
version_info=tuple(sys.version_info),
sysplatform=sys.platform,
platform=platform.platform(),
executable=sys.executable,
cwd=os.getcwd(),
)
def remote_initconfig(option_dict, args):
from _pytest.config import Config
option_dict['plugins'].append("no:terminal")
config = Config.fromdictargs(option_dict, args)
config.option.looponfail = False
config.option.usepdb = False
config.option.dist = "no"
config.option.distload = False
config.option.numprocesses = None
config.args = args
return config
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', ''))
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)
config.workerinput = workerinput
config.workeroutput = {}
# 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)