Compare commits

..

402 Commits

Author SHA1 Message Date
Bruno Oliveira
92a76bb013 Release 3.1.0
Some checks failed
deploy / deploy (push) Failing after 1s
2022-12-01 17:37:59 -03:00
Bruno Oliveira
6226965dac Merge pull request #851 from nicoddemus/789-default-dist-mode
Let users configure dist mode in the configuration file
2022-12-01 17:16:09 -03:00
Bruno Oliveira
7a0bc4c1c9 Let users configure dist mode in the configuration file
Fix #789
2022-11-30 13:57:48 -03:00
pre-commit-ci[bot]
c6bcd204ea [pre-commit.ci] pre-commit autoupdate (#849)
updates:
- [github.com/PyCQA/autoflake: v1.7.7 → v2.0.0](https://github.com/PyCQA/autoflake/compare/v1.7.7...v2.0.0)
- [github.com/pre-commit/pre-commit-hooks: v4.3.0 → v4.4.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.3.0...v4.4.0)
- [github.com/PyCQA/flake8: 5.0.4 → 6.0.0](https://github.com/PyCQA/flake8/compare/5.0.4...6.0.0)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2022-11-29 17:10:17 -03:00
Bread Genie
99c80c37a4 Fix typo psutils -> psutil (#848) 2022-11-24 09:50:25 -03:00
pre-commit-ci[bot]
e14895a96e [pre-commit.ci] pre-commit autoupdate (#846)
updates:
- [github.com/pre-commit/mirrors-mypy: v0.990 → v0.991](https://github.com/pre-commit/mirrors-mypy/compare/v0.990...v0.991)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2022-11-21 21:37:52 -03:00
Bruno Oliveira
bb27210cf9 Merge pull request #844 from pytest-dev/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2022-11-14 23:35:32 -03:00
Bruno Oliveira
4a33933e30 Use ternary operator to remove mypy error
Otherwise we would get this error:

```
error: Function "Callable[[Any, bool, Optional[float]], None]" could always be true in boolean context  [truthy-function]
```
2022-11-14 23:13:38 -03:00
pre-commit-ci[bot]
41620d2f46 [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/asottile/pyupgrade: v3.2.0 → v3.2.2](https://github.com/asottile/pyupgrade/compare/v3.2.0...v3.2.2)
- [github.com/pre-commit/mirrors-mypy: v0.982 → v0.990](https://github.com/pre-commit/mirrors-mypy/compare/v0.982...v0.990)
2022-11-15 00:17:50 +00:00
Bruno Oliveira
6b6f133850 Merge pull request #842 from nicoddemus/drop-py36-add-py311
Drop Python 3.6 and add Python 3.11
2022-11-05 14:36:30 -03:00
Bruno Oliveira
0316baf3ec Add CHANGELOG for py36 drop and py311 support 2022-11-05 12:24:00 -03:00
Bruno Oliveira
aee850a121 Add official support for Python 3.11 2022-11-05 12:24:00 -03:00
Bruno Oliveira
66993ecada Split workflows into test and deploy 2022-11-05 12:24:00 -03:00
Bruno Oliveira
2ffb426178 Drop Python 3.6 and use Python 3.10 for secondary builds 2022-11-05 12:05:30 -03:00
Bruno Oliveira
d7d396a7f9 Run pyupgrade --py37-plus 2022-11-05 12:01:07 -03:00
Thomas Grainger
59457cc1bd Update pre-commit
Also:

* Configure flake8 to ignore formatting-related problems (let black worry about them).
* Fix reference to Python 3.10 in CI,

Co-authored-by: Bruno Oliveira <nicoddemus@gmail.com>
2022-11-05 11:58:57 -03:00
Bruno Oliveira
e232b7886a Merge pull request #838 from pytest-dev/hroncok-patch-1
Remove deprecated stuff from the package description
2022-10-28 13:52:42 -03:00
Miro Hrončok
873c1fdd5e Remove deprecated stuff from the package description
Fixes https://github.com/pytest-dev/pytest-xdist/issues/835
2022-10-28 16:29:45 +02:00
Bruno Oliveira
7949c14f67 Merge pull request #830 from nicoddemus/release-3.0.0
Release 3.0.2
2022-10-25 12:58:44 -03:00
Bruno Oliveira
eed37d4771 Update CHANGELOG
Some checks failed
build / build (ubuntu-latest, 3.10-dev, py310-pytestlatest) (push) Failing after 1s
build / build (ubuntu-latest, 3.6, py36-pytestlatest) (push) Failing after 1s
build / build (ubuntu-latest, 3.7, py37-pytestlatest) (push) Failing after 1s
build / build (ubuntu-latest, 3.8, py38-psutil) (push) Failing after 1s
build / build (ubuntu-latest, 3.8, py38-pytestlatest) (push) Failing after 1s
build / build (ubuntu-latest, 3.8, py38-pytestmain) (push) Failing after 1s
build / build (ubuntu-latest, 3.8, py38-setproctitle) (push) Failing after 1s
build / build (ubuntu-latest, 3.9, py39-pytestlatest) (push) Failing after 1s
build / build (windows-latest, 3.10-dev, py310-pytestlatest) (push) Has been cancelled
build / build (windows-latest, 3.6, py36-pytestlatest) (push) Has been cancelled
build / build (windows-latest, 3.7, py37-pytestlatest) (push) Has been cancelled
build / build (windows-latest, 3.8, py38-psutil) (push) Has been cancelled
build / build (windows-latest, 3.8, py38-pytestlatest) (push) Has been cancelled
build / build (windows-latest, 3.8, py38-pytestmain) (push) Has been cancelled
build / build (windows-latest, 3.8, py38-setproctitle) (push) Has been cancelled
build / build (windows-latest, 3.9, py39-pytestlatest) (push) Has been cancelled
build / deploy (push) Has been cancelled
2022-10-25 12:36:11 -03:00
Bruno Oliveira
3e9284b6e0 Merge remote-tracking branch 'upstream/master' into release-3.0.0 2022-10-25 12:35:32 -03:00
Bruno Oliveira
ccdab727a2 Merge pull request #831 from pytest-dev/fix-setup
fix and update packaging
2022-10-25 12:32:31 -03:00
Ronny Pfannschmidt
0c981d3547 fix and update packaging
* drop empty setup.py which "breaks" on incomplete build envs
* use the build package to build the dist
* update the gh workflow to use it

fixup noise from pre-commit
2022-10-25 17:07:47 +02:00
Bruno Oliveira
38dcf52ae6 Release 3.0.1
Some checks failed
build / build (ubuntu-latest, 3.10-dev, py310-pytestlatest) (push) Failing after 1s
build / build (ubuntu-latest, 3.6, py36-pytestlatest) (push) Failing after 1s
build / build (ubuntu-latest, 3.7, py37-pytestlatest) (push) Failing after 1s
build / build (ubuntu-latest, 3.8, py38-psutil) (push) Failing after 1s
build / build (ubuntu-latest, 3.8, py38-pytestlatest) (push) Failing after 1s
build / build (ubuntu-latest, 3.8, py38-pytestmain) (push) Failing after 1s
build / build (ubuntu-latest, 3.8, py38-setproctitle) (push) Failing after 1s
build / build (ubuntu-latest, 3.9, py39-pytestlatest) (push) Failing after 2s
build / build (windows-latest, 3.10-dev, py310-pytestlatest) (push) Has been cancelled
build / build (windows-latest, 3.6, py36-pytestlatest) (push) Has been cancelled
build / build (windows-latest, 3.7, py37-pytestlatest) (push) Has been cancelled
build / build (windows-latest, 3.8, py38-psutil) (push) Has been cancelled
build / build (windows-latest, 3.8, py38-pytestlatest) (push) Has been cancelled
build / build (windows-latest, 3.8, py38-pytestmain) (push) Has been cancelled
build / build (windows-latest, 3.8, py38-setproctitle) (push) Has been cancelled
build / build (windows-latest, 3.9, py39-pytestlatest) (push) Has been cancelled
build / deploy (push) Has been cancelled
2022-10-25 11:10:07 -03:00
Bruno Oliveira
f2633f1b37 Fetch all tags, as required by setuptools-scm 2022-10-25 11:09:53 -03:00
Bruno Oliveira
192193197b Update CHANGELOG for 3.0
Some checks failed
build / build (ubuntu-latest, 3.10-dev, py310-pytestlatest) (push) Failing after 3s
build / build (ubuntu-latest, 3.6, py36-pytestlatest) (push) Failing after 2s
build / build (ubuntu-latest, 3.7, py37-pytestlatest) (push) Failing after 3s
build / build (ubuntu-latest, 3.8, py38-psutil) (push) Failing after 2s
build / build (ubuntu-latest, 3.8, py38-pytestlatest) (push) Failing after 2s
build / build (ubuntu-latest, 3.8, py38-pytestmain) (push) Failing after 2s
build / build (ubuntu-latest, 3.8, py38-setproctitle) (push) Failing after 3s
build / build (ubuntu-latest, 3.9, py39-pytestlatest) (push) Failing after 3s
build / build (windows-latest, 3.10-dev, py310-pytestlatest) (push) Has been cancelled
build / build (windows-latest, 3.6, py36-pytestlatest) (push) Has been cancelled
build / build (windows-latest, 3.7, py37-pytestlatest) (push) Has been cancelled
build / build (windows-latest, 3.8, py38-psutil) (push) Has been cancelled
build / build (windows-latest, 3.8, py38-pytestlatest) (push) Has been cancelled
build / build (windows-latest, 3.8, py38-pytestmain) (push) Has been cancelled
build / build (windows-latest, 3.8, py38-setproctitle) (push) Has been cancelled
build / build (windows-latest, 3.9, py39-pytestlatest) (push) Has been cancelled
build / deploy (push) Has been cancelled
2022-10-24 13:44:11 -03:00
Bruno Oliveira
0f58a14def Fix towncrier command for latest version 2022-10-24 13:43:03 -03:00
Bruno Oliveira
bd23c2460f Fix changelog fragments 2022-10-24 13:43:03 -03:00
Bruno Oliveira
794f28bcf4 Use modern syntax for towncrier 2022-10-24 13:43:03 -03:00
Bruno Oliveira
31e80c8c6a Merge pull request #828 from nicoddemus/deprecate-rsync-825
Deprecate rsync support
2022-10-24 13:02:58 -03:00
Bruno Oliveira
1e9f6a7c3c Fix type annotation for pytest 6.2 2022-10-24 12:03:14 -03:00
Bruno Oliveira
ed65e888cf Deprecate rsync support
Fix #825
2022-10-24 11:53:15 -03:00
Petr Viktorin
c168706fbe Get -nauto default from PYTEST_XDIST_AUTO_NUM_WORKERS (#829)
* Get `-nauto` default from `PYTEST_XDIST_AUTO_NUM_WORKERS`

A few additional tests for existing functionality are added too.
And the ``-n logical`` option is documented.

Fixes: https://github.com/pytest-dev/pytest-xdist/issues/792
2022-10-24 09:33:19 -03:00
Bruno Oliveira
ab95a4b45d Merge pull request #827 from nicoddemus/deprecate-looponfail
Deprecate looponfail plugin
2022-10-22 13:31:06 -03:00
Bruno Oliveira
8b0856a986 Deprecate looponfail plugin
Fix #826
2022-10-22 10:43:02 -03:00
Bruno Oliveira
f2064873fd Merge pull request #821 from nicoddemus/drop-forked
Remove --boxed and pytest-forked dependency
2022-10-22 10:04:40 -03:00
Bruno Oliveira
1fbf49909f Remove --boxed and pytest-forked dependency
Fixes #468
Fixes #543
2022-10-22 07:31:34 -03:00
Anthony Sottile
1471164197 Replace usages of py.log by custom implementation (#822)
Co-authored-by: Bruno Oliveira <nicoddemus@gmail.com>
2022-10-22 07:26:01 -03:00
Aleksandr Mezin
80130d59ea Restore old initial batch distribution logic in LoadScheduling (#812)
* Restore old initial batch distribution logic in LoadScheduling

pytest orders tests for optimal sequential execution - i. e. avoiding
unnecessary setup and teardown of fixtures. So executing tests in consecutive
chunks is important for optimal performance.

Commit 09d79ace35 optimized test distribution for
the corner case, when the number of tests is less than 2 * number of nodes.
At the same time, it made initial test distribution worse for all other cases.
If some tests use some fixture, and these tests fit into the initial batch,
the fixture will be created min(n_tests, n_workers) times, no matter how many
other tests there are. With the old algorithm (before
09d79ace35), if there are enough tests not using
the fixture, the fixture was created only once.

So restore the old behavior for typical cases where the number of tests is
much greater than the number of workers (or, strictly speaking, when there
are at least 2 tests for every node).

In my test suite, where fixtures create Docker containers, this change reduces
total run time by 10-15%.

This is a partial revert of commit 09d79ace35

Co-authored-by: Bruno Oliveira <nicoddemus@gmail.com>
2022-10-22 07:12:16 -03:00
Bruno Oliveira
a1785b539c Merge pull request #824 from nicoddemus/drop-py-path
Replace py.path.local usages by pathlib.Path
2022-10-22 07:00:42 -03:00
Bruno Oliveira
25cc1a4119 Replace py.path.local usages by pathlib.Path 2022-10-21 22:54:17 -03:00
Anthony Sottile
f04cd22fd6 Merge pull request #823 from pytest-dev/remove-pydir
remove py._pydir as rsync candidate
2022-10-21 20:38:22 -04:00
Anthony Sottile
42024f374a remove py._pydir as rsync candidate 2022-10-21 16:09:14 -07:00
Aleksandr Mezin
e407a3377e Cancel shutdown when crashed worker is restarted (#813)
Co-authored-by: Bruno Oliveira <nicoddemus@gmail.com>
2022-09-06 17:37:38 -03:00
Bruno Oliveira
6169d494a4 Merge pull request #516 from blueyed/test_collection_crash 2022-09-06 17:29:28 -03:00
Daniel Hahler
d137384315 Add test_collection_crash
Reproduces:

    INTERNALERROR> TypeError: unhashable type: 'ExceptionChainRepr'

Ref: https://github.com/pytest-dev/pytest-xdist/issues/515
Ref: https://github.com/pytest-dev/pytest/pull/6988
2022-09-06 17:20:44 -03:00
Bruno Oliveira
23992d874e Merge pull request #807 from pytest-dev/no-capture-docs 2022-09-06 17:20:20 -03:00
Bruno Oliveira
2ec4f407f5 Add known limitation about -s not working 2022-08-24 12:52:36 -03:00
Sylvain Marié
9236f11757 Added a couple tests for #404 (#766)
* Added a couple tests for #404
Co-authored-by: Sylvain MARIE <sylvain.marie@se.com>
2022-08-23 21:41:23 +01:00
Pepa
61132777f8 Added known limitation section to the docs (#796)
Co-authored-by: Josef <sorry.i-keep@it.private>
Co-authored-by: Bruno Oliveira <nicoddemus@gmail.com>
2022-07-22 10:44:17 -03:00
Bruno Oliveira
8616ff368c Merge pull request #793 from pytest-dev/fix-setuptools-scm 2022-07-13 15:55:17 -03:00
Petr Viktorin
1a5e680e91 Document the pytest_xdist_auto_num_workers hook (#791) 2022-07-13 15:54:26 -03:00
Ronny Pfannschmidt
9d69c1a7da correct setuptools_scm support
modern setuptools_scm drops python 3.6

* use build isolation
* drop setup_requires
2022-07-12 16:54:51 +02:00
Bruno Oliveira
8a417f4545 Merge pull request #785 from pytest-dev/pre-commit-ci-update-config 2022-05-31 08:05:58 -03:00
pre-commit-ci[bot]
44cbd7fdc5 [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/pre-commit/mirrors-mypy: v0.950 → v0.960](https://github.com/pre-commit/mirrors-mypy/compare/v0.950...v0.960)
2022-05-30 21:01:27 +00:00
Bruno Oliveira
37c06dae24 Merge pull request #782 from pytest-dev/pre-commit-ci-update-config 2022-05-10 19:14:57 -03:00
pre-commit-ci[bot]
2220ccecb1 [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/asottile/pyupgrade: v2.32.0 → v2.32.1](https://github.com/asottile/pyupgrade/compare/v2.32.0...v2.32.1)
2022-05-09 20:58:23 +00:00
pre-commit-ci[bot]
7743e14fd4 [pre-commit.ci] pre-commit autoupdate (#781)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2022-05-04 09:21:39 -03:00
auziel
8652ef91e6 Fix the example of using --rsyncdir option (#777)
Co-authored-by: Ronny Pfannschmidt <opensource@ronnypfannschmidt.de>
Co-authored-by: Bruno Oliveira <nicoddemus@gmail.com>
2022-04-20 18:00:28 -03:00
Bruno Oliveira
e526c38311 Merge pull request #776 from pytest-dev/pre-commit-ci-update-config 2022-04-12 08:05:34 -03:00
pre-commit-ci[bot]
8723495e9b [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/pre-commit/pre-commit-hooks: v4.1.0 → v4.2.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.1.0...v4.2.0)
- [github.com/asottile/pyupgrade: v2.31.1 → v2.32.0](https://github.com/asottile/pyupgrade/compare/v2.31.1...v2.32.0)
2022-04-11 21:29:34 +00:00
Bruno Oliveira
48a213b47c Merge pull request #774 from pytest-dev/pre-commit-ci-update-config 2022-04-04 18:18:41 -03:00
pre-commit-ci[bot]
eca7f202b6 [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/psf/black: 22.1.0 → 22.3.0](https://github.com/psf/black/compare/22.1.0...22.3.0)
2022-04-04 20:28:36 +00:00
Ronny Pfannschmidt
3fc253f192 Merge pull request #770 from pytest-dev/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2022-03-28 22:02:14 +02:00
pre-commit-ci[bot]
6fca8686f5 [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/pre-commit/mirrors-mypy: v0.941 → v0.942](https://github.com/pre-commit/mirrors-mypy/compare/v0.941...v0.942)
2022-03-28 19:38:44 +00:00
Bruno Oliveira
9c7b5f4831 Merge pull request #768 from pytest-dev/pre-commit-ci-update-config 2022-03-22 09:09:23 -03:00
pre-commit-ci[bot]
2fefa36db6 [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/pre-commit/mirrors-mypy: v0.940 → v0.941](https://github.com/pre-commit/mirrors-mypy/compare/v0.940...v0.941)
2022-03-21 23:42:02 +00:00
Bruno Oliveira
1102b82795 Merge pull request #765 from pytest-dev/pre-commit-ci-update-config 2022-03-15 16:39:52 -03:00
pre-commit-ci[bot]
a685a04bd1 [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/asottile/blacken-docs: v1.12.0 → v1.12.1](https://github.com/asottile/blacken-docs/compare/v1.12.0...v1.12.1)
- [github.com/asottile/pyupgrade: v2.31.0 → v2.31.1](https://github.com/asottile/pyupgrade/compare/v2.31.0...v2.31.1)
- [github.com/pre-commit/mirrors-mypy: v0.931 → v0.940](https://github.com/pre-commit/mirrors-mypy/compare/v0.931...v0.940)
2022-03-14 23:27:19 +00:00
Bruno Oliveira
41a4d439c8 Fix typo in docs 2022-03-10 12:24:32 -03:00
Bruno Oliveira
24f331ccf0 Merge pull request #764 from nicoddemus/fix-log-worker 2022-03-10 11:55:19 -03:00
Bruno Oliveira
34f63600ee Simplify the example about creating one log per worker
No need to use a custom option as this complicates the example a bit and might distract from the main details.
2022-03-10 11:52:07 -03:00
Bruno Oliveira
28688962e1 Merge pull request #762 from marcosanchotene/multiple-log-files 2022-03-10 09:38:24 -03:00
Bruno Oliveira
7f04ce35c9 Add blacken-docs and use autodoc 2022-03-10 09:33:23 -03:00
Bruno Oliveira
ad5468b6e7 Add blacken docs pre-commit hook 2022-03-10 09:26:16 -03:00
Bruno Oliveira
797867f23d Simplify example and use syntax for env vars 2022-03-10 09:25:37 -03:00
Bruno Oliveira
e3c1a3de67 Remove redundant type casts 2022-03-10 09:11:00 -03:00
Marco Sanchotene
14e1d80f10 Add instructions to create one file for each worker 2022-03-08 19:11:48 -03:00
Bruno Oliveira
29c4cce1d4 Merge pull request #757 from kianmeng/fix-typos 2022-02-07 20:29:59 -03:00
Kian-Meng Ang
d79ef915a2 Fix typos 2022-02-07 21:59:38 +08:00
pre-commit-ci[bot]
290b322a5d [pre-commit.ci] pre-commit autoupdate (#755)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2022-02-01 08:34:13 -03:00
Ran Benita
8fcf927c44 Merge pull request #748 from bluetech/changelog
CHANGELOG: add `--boxed` deprecation to 2.5.0
2022-01-31 15:30:33 +02:00
Bruno Oliveira
8cd10225df Add sphinx docs folder and changelog URLs to metadata (#751) 2022-01-29 16:29:36 +00:00
Bruno Oliveira
1a5ad8ac20 Merge pull request #750 from ssbarnea/fix/maxprocesses 2022-01-29 08:04:12 -03:00
Sorin Sbarnea
dd61dce892 Document two missing arguments
Fixes: #614
2022-01-29 10:47:05 +00:00
Ran Benita
2aeb5d51f5 CHANGELOG: add --boxed deprecation to 2.5.0 2022-01-24 16:14:24 +02:00
Bruno Oliveira
a1a6c77028 Merge pull request #746 from pytest-dev/pre-commit-ci-update-config 2022-01-11 07:22:27 -03:00
pre-commit-ci[bot]
628a04de8e [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/pre-commit/mirrors-mypy: v0.930 → v0.931](https://github.com/pre-commit/mirrors-mypy/compare/v0.930...v0.931)
2022-01-10 21:28:28 +00:00
Bruno Oliveira
5749a347b9 Merge pull request #745 from pytest-dev/pre-commit-ci-update-config 2022-01-04 09:24:43 -03:00
pre-commit-ci[bot]
c4e0d39f21 [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/asottile/pyupgrade: v2.29.1 → v2.31.0](https://github.com/asottile/pyupgrade/compare/v2.29.1...v2.31.0)
2022-01-03 21:09:27 +00:00
Bruno Oliveira
d709e4cda0 Merge pull request #744 from pytest-dev/pre-commit-ci-update-config 2021-12-27 19:00:50 -03:00
pre-commit-ci[bot]
1c7fc1a812 [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/pre-commit/pre-commit-hooks: v4.0.1 → v4.1.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.0.1...v4.1.0)
- [github.com/pre-commit/mirrors-mypy: v0.920 → v0.930](https://github.com/pre-commit/mirrors-mypy/compare/v0.920...v0.930)
2021-12-27 21:13:26 +00:00
Bruno Oliveira
6b02b353d2 Merge pull request #743 from pytest-dev/pre-commit-ci-update-config 2021-12-27 09:29:49 -03:00
Bruno Oliveira
18143275c6 Remove obsolete skip based on java (no longer supported) 2021-12-27 08:05:46 -03:00
Bruno Oliveira
d21f59fea7 Fix typing errors after updating mypy 2021-12-27 08:05:01 -03:00
pre-commit-ci[bot]
82ecdea0c2 [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/pre-commit/mirrors-mypy: v0.910-1 → v0.920](https://github.com/pre-commit/mirrors-mypy/compare/v0.910-1...v0.920)
2021-12-20 20:42:50 +00:00
Bruno Oliveira
f9f8a765ba Merge pull request #741 from nicoddemus/release-2.5.0 2021-12-10 08:45:48 -03:00
Bruno Oliveira
13f39349c6 Remove unnecessary skip from test_logfinish_hook as we require pytest>=6.2
Some checks failed
build / build (ubuntu-latest, 3.10-dev, py310-pytestlatest) (push) Failing after 2s
build / build (ubuntu-latest, 3.6, py36-pytestlatest) (push) Failing after 2s
build / build (ubuntu-latest, 3.7, py37-pytestlatest) (push) Failing after 1s
build / build (ubuntu-latest, 3.8, py38-psutil) (push) Failing after 2s
build / build (ubuntu-latest, 3.8, py38-pytestlatest) (push) Failing after 1s
build / build (ubuntu-latest, 3.8, py38-pytestmain) (push) Failing after 2s
build / build (ubuntu-latest, 3.8, py38-setproctitle) (push) Failing after 2s
build / build (ubuntu-latest, 3.9, py39-pytestlatest) (push) Failing after 3s
build / build (windows-latest, 3.10-dev, py310-pytestlatest) (push) Has been cancelled
build / build (windows-latest, 3.6, py36-pytestlatest) (push) Has been cancelled
build / build (windows-latest, 3.7, py37-pytestlatest) (push) Has been cancelled
build / build (windows-latest, 3.8, py38-psutil) (push) Has been cancelled
build / build (windows-latest, 3.8, py38-pytestlatest) (push) Has been cancelled
build / build (windows-latest, 3.8, py38-pytestmain) (push) Has been cancelled
build / build (windows-latest, 3.8, py38-setproctitle) (push) Has been cancelled
build / build (windows-latest, 3.9, py39-pytestlatest) (push) Has been cancelled
build / deploy (push) Has been cancelled
2021-12-10 08:32:32 -03:00
Bruno Oliveira
c76d5622f1 Skip test_warning_captured_deprecated_in_pytest_6 in pytest>=7.1 2021-12-10 08:31:55 -03:00
Bruno Oliveira
5f78c7155e Fix CHANGELOG header 2021-12-10 08:19:30 -03:00
Bruno Oliveira
c8bbc03e49 Release 2.5.0 2021-12-10 08:18:22 -03:00
Bruno Oliveira
8dbf3677dc Merge pull request #738 from pytest-dev/pre-commit-ci-update-config 2021-12-06 18:58:17 -03:00
pre-commit-ci[bot]
a25c14bef5 [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/psf/black: 21.11b1 → 21.12b0](https://github.com/psf/black/compare/21.11b1...21.12b0)
2021-12-06 21:11:40 +00:00
Bruno Oliveira
110c114025 Merge pull request #734 from nicoddemus/revamp-readme 2021-12-02 10:07:49 -03:00
Bruno Oliveira
83bdbf4b95 Revamp README
* Use a document title.
* Show a "short and sweet" section at the beginning highlighting the main usage of the plugin.
* Add a table of contents.
* Use a dedicated howto section at the end.
* Move the OVERVIEW section to the main README.
2021-11-29 13:13:20 -03:00
Bruno Oliveira
630c1eb6f2 Merge pull request #733 from baekdohyeop/feature-loadgroup 2021-11-29 12:00:59 -03:00
baekdohyeop
62e50d0097 Address review 2021-11-29 02:59:18 +09:00
baekdohyeop
1b5d6b6db7 Add changelog 2021-11-29 02:59:18 +09:00
baekdohyeop
52a395888f Create new dist option 'loadgroup' 2021-11-29 02:59:18 +09:00
Bruno Oliveira
6d83034bb8 Fix test_warning_captured_deprecated_in_pytest_6
This test started to fail in the 'py38-pytestmain' environment, the
cause being PytestRemovedIn7Warning being raised by the conftest file
of the test itself. Ignoring it is fine, the purpose of the test is to ensure
the hook is not called by pytest-xdist.
2021-11-29 02:59:18 +09:00
Bruno Oliveira
408eb1cfec Merge pull request #732 from nicoddemus/fix-warning-captured-test 2021-11-28 11:15:04 -03:00
Bruno Oliveira
1e150204e0 Fix test_warning_captured_deprecated_in_pytest_6
This test started to fail in the 'py38-pytestmain' environment, the
cause being PytestRemovedIn7Warning being raised by the conftest file
of the test itself. Ignoring it is fine, the purpose of the test is to ensure
the hook is not called by pytest-xdist.
2021-11-28 10:34:14 -03:00
Bruno Oliveira
e2a9381036 Merge pull request #731 from pytest-dev/pre-commit-ci-update-config 2021-11-28 10:31:05 -03:00
pre-commit-ci[bot]
982de538c0 [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/psf/black: 21.10b0 → 21.11b1](https://github.com/psf/black/compare/21.10b0...21.11b1)
- [github.com/asottile/pyupgrade: v2.29.0 → v2.29.1](https://github.com/asottile/pyupgrade/compare/v2.29.0...v2.29.1)
2021-11-22 20:53:14 +00:00
Ran Benita
2bac85562b Merge pull request #728 from bluetech/hookimpl-decorators
Add explicit pytest.hookspec/hookimpl annotations, avoid legacy tryfirst/trylast marks
2021-11-02 12:28:36 +02:00
Ran Benita
b2586c3f48 Merge pull request #727 from bluetech/deprecate-boxed
Deprecate `--boxed`
2021-11-02 12:28:19 +02:00
Ran Benita
b85b71cd22 Add explicit pytest.hookspec/hookimpl annotations, avoid legacy tryfirst/trylast marks 2021-11-02 09:42:00 +02:00
Ran Benita
89a3df3e20 Deprecate --boxed
This will allow removing the pytest-forked dependency in the future.

Refs #468.
2021-11-02 09:38:56 +02:00
Bruno Oliveira
39eec49ddf Merge pull request #726 from pytest-dev/pre-commit-ci-update-config 2021-11-01 19:02:26 -03:00
pre-commit-ci[bot]
bf9606c672 [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/psf/black: 21.9b0 → 21.10b0](https://github.com/psf/black/compare/21.9b0...21.10b0)
2021-11-01 20:27:36 +00:00
Ran Benita
18cd9b900c Merge pull request #725 from bluetech/docs-master
README,OVERVIEW: replace master -> controller
2021-11-01 18:27:19 +02:00
Bruno Oliveira
10d8198d88 Merge pull request #724 from pytest-dev/bluetech-patch-1 2021-11-01 08:08:20 -03:00
Ran Benita
475d7c4c2a README,OVERVIEW: replace master -> controller 2021-11-01 09:36:12 +02:00
Ran Benita
0fac79b29c README: fill in TBD version 2021-11-01 09:24:59 +02:00
Ran Benita
827909b4e1 Merge pull request #723 from bluetech/modernize
Modernize packaging, require pytest 6.2, prepare for pytest 7, other changes
2021-11-01 09:19:04 +02:00
Ran Benita
ae58690f3e Avoid soft-deprecated pathlist ini type on pytest>=7
This allows the test suite to succeed with the legacypath plugin
blocked.

Fix #722.
2021-11-01 09:06:32 +02:00
Ran Benita
d794b27e72 Use config._tmp_path_factory instead of config._tmpdirhandler
Both are private, and shouldn't be used, but not sure how to fix it
currently. But `_tmpdirhandler` will be relegated to the legacypath
plugin so should extra not be used.
2021-11-01 09:06:32 +02:00
Ran Benita
787b8d9c14 Add basic mypy linting
Based on pytest setup, though more lax. Currently only the tests are
annotated so only helpful for them.

Fix #721.
2021-11-01 09:06:32 +02:00
Ran Benita
9ddb274f23 Update test suite to modern pytest
- Use pytest>=6.2 features (same as install_requires).
  When we can require >=7, can fix some more typing omissions and
  version checks.
- Replace testdir with pytester
- Replace py.test with pytest
- Replace tmpdir with tmp_path
- Remove (almost) all other uses of py
- Add some type annotations (not checked yet)

Ref #722.
2021-11-01 09:05:28 +02:00
Ran Benita
5672d85809 Require pytest>=6.2
This makes things much easier in terms of typing.

pytest 6.2 has been out for a ~year so if someone updates pytest-xdist
they should also update pytest!

Fix #720.
2021-11-01 09:04:16 +02:00
Ran Benita
999923ba97 Allow black and rst pre-commit hooks to run on any Python 3 version
Since py2 is dropped we don't really need to worry about this anymore.
2021-11-01 09:04:16 +02:00
Ran Benita
f51289582e Bring back tox -e linting
It just runs pre-commit, and CI doesn't use i,t but I still find it
useful for local development.
2021-11-01 09:04:16 +02:00
Ran Benita
130dcdef76 Add build-system to pyproject.toml
So doesn't use legacy stuff.

Fix #719.
2021-11-01 09:04:14 +02:00
Ran Benita
f8a434e349 Move metadata from setup.py to setup.cfg
Using https://github.com/asottile/setup-py-upgrade

Refs #719.
2021-10-30 21:04:10 +03:00
Bruno Oliveira
b2f3d08106 Merge pull request #715 from pytest-dev/pre-commit-ci-update-config 2021-10-11 17:03:57 -03:00
pre-commit-ci[bot]
4d902461f9 [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/PyCQA/flake8: 3.9.2 → 4.0.1](https://github.com/PyCQA/flake8/compare/3.9.2...4.0.1)
2021-10-11 19:44:07 +00:00
Bruno Oliveira
15b411e94b Merge pull request #713 from pytest-dev/pre-commit-ci-update-config 2021-10-04 18:57:14 -03:00
pre-commit-ci[bot]
3fa5431a53 [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/asottile/pyupgrade: v2.28.0 → v2.29.0](https://github.com/asottile/pyupgrade/compare/v2.28.0...v2.29.0)
2021-10-04 19:50:07 +00:00
Bruno Oliveira
640c85a70a Merge pull request #710 from pytest-dev/pre-commit-ci-update-config 2021-09-27 22:52:42 -03:00
pre-commit-ci[bot]
c0ce0c0171 [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/asottile/pyupgrade: v2.26.0 → v2.28.0](https://github.com/asottile/pyupgrade/compare/v2.26.0...v2.28.0)
2021-09-27 19:44:31 +00:00
Bruno Oliveira
9943887304 Merge pull request #708 from nicoddemus/use-modern-hook-opts 2021-09-24 10:18:39 -03:00
Bruno Oliveira
c7c7f9ef5b Use modern options configurations for hooks
Noticed while reviewing https://github.com/pytest-dev/pytest/pull/9118
2021-09-24 10:09:24 -03:00
Bruno Oliveira
56275d4418 Merge pull request #701 from nicoddemus/release-2.4.0 2021-09-21 09:26:20 -03:00
Bruno Oliveira
4097c343c2 Add final note to merge PR in RELEASING.rst 2021-09-21 09:21:51 -03:00
Bruno Oliveira
4b487ed5e1 Manually fix changelog title
Some checks failed
build / build (ubuntu-latest, 3.10-dev, py310-pytestlatest) (push) Failing after 3s
build / build (ubuntu-latest, 3.6, py36-pytestlatest) (push) Failing after 2s
build / build (ubuntu-latest, 3.7, py37-pytestlatest) (push) Failing after 2s
build / build (ubuntu-latest, 3.8, py38-psutil) (push) Failing after 2s
build / build (ubuntu-latest, 3.8, py38-pytestlatest) (push) Failing after 2s
build / build (ubuntu-latest, 3.8, py38-pytestmain) (push) Failing after 3s
build / build (ubuntu-latest, 3.8, py38-setproctitle) (push) Failing after 2s
build / build (ubuntu-latest, 3.9, py39-pytestlatest) (push) Failing after 2s
build / build (windows-latest, 3.10-dev, py310-pytestlatest) (push) Has been cancelled
build / build (windows-latest, 3.6, py36-pytestlatest) (push) Has been cancelled
build / build (windows-latest, 3.7, py37-pytestlatest) (push) Has been cancelled
build / build (windows-latest, 3.8, py38-psutil) (push) Has been cancelled
build / build (windows-latest, 3.8, py38-pytestlatest) (push) Has been cancelled
build / build (windows-latest, 3.8, py38-pytestmain) (push) Has been cancelled
build / build (windows-latest, 3.8, py38-setproctitle) (push) Has been cancelled
build / build (windows-latest, 3.9, py39-pytestlatest) (push) Has been cancelled
build / deploy (push) Has been cancelled
2021-09-20 21:19:04 -03:00
Bruno Oliveira
ecf4d3be31 Release 2.4.0 2021-09-20 21:17:31 -03:00
Bruno Oliveira
87d89795e3 Merge pull request #704 from hugovk/add-3.10 2021-09-20 21:16:42 -03:00
Bruno Oliveira
b4544c8849 Merge pull request #706 from pytest-dev/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2021-09-20 21:04:22 -03:00
pre-commit-ci[bot]
66dc3904b4 [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/psf/black: 21.8b0 → 21.9b0](https://github.com/psf/black/compare/21.8b0...21.9b0)
2021-09-20 20:00:03 +00:00
Hugo van Kemenade
e0ce1b7418 Add news file to add support for Python 3.10 2021-09-15 17:28:44 +03:00
Hugo van Kemenade
ed47f0e01a Add support for Python 3.10 2021-09-15 17:14:59 +03:00
Bruno Oliveira
1c8178abc5 Merge pull request #703 from pytest-dev/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2021-09-15 08:52:56 -03:00
pre-commit-ci[bot]
9807064f66 [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/asottile/pyupgrade: v2.25.0 → v2.26.0](https://github.com/asottile/pyupgrade/compare/v2.25.0...v2.26.0)
2021-09-13 19:41:01 +00:00
David Lamparter
766e67ce52 Use setproctitle if available to show state (#696)
Co-authored-by: Bruno Oliveira <nicoddemus@gmail.com>
2021-09-03 12:58:01 -03:00
Bruno Oliveira
305aeeacec Merge pull request #700 from nicoddemus/fix-pytestmaster
Fix test_fixture_teardown_failure for pytest master
2021-09-03 12:45:44 -03:00
Bruno Oliveira
90fc616906 Fix test_fixture_teardown_failure for pytest master
`--debug` now receives an optional file, so the way the test
was calling pytest made it seem like it was passing the python file
file as the debug file.
2021-09-01 07:54:13 -03:00
Bruno Oliveira
5f4d6ea0e6 Merge pull request #699 from pytest-dev/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2021-09-01 07:50:56 -03:00
pre-commit-ci[bot]
a0ad5cf041 [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/psf/black: 21.7b0 → 21.8b0](https://github.com/psf/black/compare/21.7b0...21.8b0)
- [github.com/asottile/pyupgrade: v2.24.0 → v2.25.0](https://github.com/asottile/pyupgrade/compare/v2.24.0...v2.25.0)
2021-08-30 19:26:09 +00:00
Bruno Oliveira
5e0b40cea2 Merge pull request #694 from pytest-dev/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2021-08-24 08:08:18 -03:00
pre-commit-ci[bot]
513e203ee1 [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/asottile/pyupgrade: v2.23.0 → v2.24.0](https://github.com/asottile/pyupgrade/compare/v2.23.0...v2.24.0)
2021-08-23 18:59:34 +00:00
Thomas Grainger
a5c65dd1e4 Merge pull request #689 from pytest-dev/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2021-07-26 19:55:07 +01:00
pre-commit-ci[bot]
ccb14f24fc [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/asottile/pyupgrade: v2.21.2 → v2.23.0](https://github.com/asottile/pyupgrade/compare/v2.21.2...v2.23.0)
2021-07-26 18:05:33 +00:00
Ronny Pfannschmidt
76dbde0475 Merge pull request #686 from pytest-dev/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2021-07-20 10:44:03 +02:00
pre-commit-ci[bot]
1167e1609c [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/psf/black: 21.6b0 → 21.7b0](https://github.com/psf/black/compare/21.6b0...21.7b0)
- [github.com/asottile/pyupgrade: v2.21.0 → v2.21.2](https://github.com/asottile/pyupgrade/compare/v2.21.0...v2.21.2)
2021-07-19 22:14:44 +00:00
Bruno Oliveira
348a956724 Merge pull request #678 from pytest-dev/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2021-07-13 08:02:41 -03:00
pre-commit-ci[bot]
dba716ba91 [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/asottile/pyupgrade: v2.19.4 → v2.21.0](https://github.com/asottile/pyupgrade/compare/v2.19.4...v2.21.0)
2021-07-12 23:28:12 +00:00
Ronny Pfannschmidt
e7c42f0b3b finish release 2.3.0 2021-06-16 23:53:52 +02:00
Ronny Pfannschmidt
fe57b39563 fixup: add release title underline for 2.3.0
Some checks failed
build / build (ubuntu-latest, 3.6, py36-pytestlatest) (push) Failing after 1s
build / build (ubuntu-latest, 3.7, py37-pytestlatest) (push) Failing after 1s
build / build (ubuntu-latest, 3.8, py38-psutil) (push) Failing after 1s
build / build (ubuntu-latest, 3.8, py38-pytestlatest) (push) Failing after 1s
build / build (ubuntu-latest, 3.8, py38-pytestmain) (push) Failing after 1s
build / build (ubuntu-latest, 3.9, py39-pytestlatest) (push) Failing after 1s
build / build (windows-latest, 3.6, py36-pytestlatest) (push) Has been cancelled
build / build (windows-latest, 3.7, py37-pytestlatest) (push) Has been cancelled
build / build (windows-latest, 3.8, py38-psutil) (push) Has been cancelled
build / build (windows-latest, 3.8, py38-pytestlatest) (push) Has been cancelled
build / build (windows-latest, 3.8, py38-pytestmain) (push) Has been cancelled
build / build (windows-latest, 3.9, py39-pytestlatest) (push) Has been cancelled
build / deploy (push) Has been cancelled
2021-06-16 11:49:15 +02:00
Ronny Pfannschmidt
26e7d953f4 prepare release 2.3.0 2021-06-16 11:46:18 +02:00
Ronny Pfannschmidt
b02a6db771 Merge pull request #667 from graingert/fix-sys-path
fix sys.path for local workers  Fixes #421
2021-06-16 11:40:55 +02:00
Thomas Grainger
b0722675e0 add newsfile 2021-06-16 10:34:19 +01:00
Bruno Oliveira
881cc48271 Merge pull request #672 from pytest-dev/pre-commit-ci-update-config 2021-06-15 09:46:15 -03:00
pre-commit-ci[bot]
958679e9a7 [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/psf/black: 21.5b2 → 21.6b0](https://github.com/psf/black/compare/21.5b2...21.6b0)
- [github.com/asottile/pyupgrade: v2.19.1 → v2.19.4](https://github.com/asottile/pyupgrade/compare/v2.19.1...v2.19.4)
2021-06-14 17:41:19 +00:00
Ronny Pfannschmidt
7f07d50808 Merge pull request #646 from kroeschl/numprocesses-logical
Add explicit 'logical' choice to --numprocesses flag
2021-06-08 08:44:29 +02:00
Ronny Pfannschmidt
fb518def55 Merge pull request #669 from pytest-dev/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2021-06-08 08:43:14 +02:00
pre-commit-ci[bot]
0b14d92894 [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/asottile/pyupgrade: v2.19.0 → v2.19.1](https://github.com/asottile/pyupgrade/compare/v2.19.0...v2.19.1)
2021-06-08 01:53:05 +00:00
Thomas Grainger
02f971d45f swap docstring 2021-06-03 21:46:21 +01:00
Thomas Grainger
56e631b4bb fix sys.path for local workers Fixes #421 2021-06-03 21:20:11 +01:00
Bruno Oliveira
7f2426b11d Merge pull request #665 from pytest-dev/pre-commit-ci-update-config 2021-05-31 16:35:11 -03:00
pre-commit-ci[bot]
0c84201b40 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2021-05-31 19:13:26 +00:00
pre-commit-ci[bot]
08d8a62040 [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/psf/black: 21.5b1 → 21.5b2](https://github.com/psf/black/compare/21.5b1...21.5b2)
- [github.com/asottile/pyupgrade: v2.16.0 → v2.19.0](https://github.com/asottile/pyupgrade/compare/v2.16.0...v2.19.0)
2021-05-31 17:33:03 +00:00
Bruno Oliveira
f36ee80e8c Merge pull request #666 from nicoddemus/296-capture-docs 2021-05-27 12:50:07 -03:00
Bruno Oliveira
f75479f7f3 Document that -s doesn't work with pytest-xdist
Fix #296
2021-05-27 10:49:38 -03:00
Ronny Pfannschmidt
10ec3af8a0 Merge pull request #662 from pytest-dev/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2021-05-17 21:34:49 +02:00
pre-commit-ci[bot]
577e24b5bd [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/pre-commit/pre-commit-hooks: v3.4.0 → v4.0.1](https://github.com/pre-commit/pre-commit-hooks/compare/v3.4.0...v4.0.1)
- [github.com/asottile/pyupgrade: v2.15.0 → v2.16.0](https://github.com/asottile/pyupgrade/compare/v2.15.0...v2.16.0)
2021-05-17 17:29:04 +00:00
Bruno Oliveira
e37039ee41 Merge pull request #660 from pytest-dev/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2021-05-13 08:11:26 -03:00
pre-commit-ci[bot]
cc828ddbfc [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/psf/black: 21.4b2 → 21.5b1](https://github.com/psf/black/compare/21.4b2...21.5b1)
- [github.com/PyCQA/flake8: 3.9.1 → 3.9.2](https://github.com/PyCQA/flake8/compare/3.9.1...3.9.2)
- [github.com/asottile/pyupgrade: v2.14.0 → v2.15.0](https://github.com/asottile/pyupgrade/compare/v2.14.0...v2.15.0)
2021-05-10 20:26:00 +00:00
Bruno Oliveira
4f459aa119 Merge pull request #656 from pytest-dev/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2021-05-04 11:24:47 -03:00
pre-commit-ci[bot]
3a7604bdb7 [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/psf/black: 21.4b0 → 21.4b2](https://github.com/psf/black/compare/21.4b0...21.4b2)
- [github.com/asottile/pyupgrade: v2.13.0 → v2.14.0](https://github.com/asottile/pyupgrade/compare/v2.13.0...v2.14.0)
2021-05-03 17:21:44 +00:00
Bruno Oliveira
d4d01bba2e Apply suggestions from code review 2021-04-27 11:51:04 -03:00
Kyle Roeschley
4d27110beb Add 'logical' CPU count choice for --numprocesses
This can significantly speed up runs when testing is not CPU-bound.
2021-04-27 08:47:21 -05:00
pre-commit-ci[bot]
260211603e [pre-commit.ci] pre-commit autoupdate (#655)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2021-04-27 09:30:37 -03:00
xoviat
f861f1667d add new pytest_handlecrashitem hook
allows handling and rescheduling crash tests

PR #651

Co-authored-by: xoviat <xoviat@users.noreply.github.com>
Co-authored-by: Bruno Oliveira <nicoddemus@gmail.com>
2021-04-27 09:07:49 -03:00
Bruno Oliveira
f5665436e9 Merge pull request #654 from nicoddemus/drop-py35
Drop Python 3.5 support
2021-04-27 08:44:56 -03:00
Bruno Oliveira
8b35945d54 Drop Python 3.5 support
pytest no longer supports it, and our CI is broken because of it.
2021-04-25 09:31:10 -03:00
Bruno Oliveira
3fe2094197 Merge pull request #649 from pytest-dev/pre-commit-ci-update-config 2021-04-24 16:19:00 -03:00
pre-commit-ci[bot]
04dde88996 [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/PyCQA/flake8: 3.9.0 → 3.9.1](https://github.com/PyCQA/flake8/compare/3.9.0...3.9.1)
- [github.com/asottile/pyupgrade: v2.11.0 → v2.12.0](https://github.com/asottile/pyupgrade/compare/v2.11.0...v2.12.0)
2021-04-19 17:12:26 +00:00
pre-commit-ci[bot]
9785a316ae [pre-commit.ci] pre-commit autoupdate (#645)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2021-04-07 10:13:14 -03:00
Thomas Grainger
1dc019709c Use 'main' to refer to pytest default branch in tox env names. (#643)
Co-authored-by: Thomas Grainger <tagrain@gmail.com>
Co-authored-by: Bruno Oliveira <nicoddemus@gmail.com>
2021-03-20 11:43:57 -03:00
Bruno Oliveira
d909dcc61f Merge pull request #642 from adamchainz/upgrade_pre_commit 2021-03-20 11:34:20 -03:00
Adam Johnson
f02aa70c95 Upgrade pre-commit hooks
Use `pre-commit autoupdate` to upgrade, and follow the message from `pre-commit-hooks` to move to the official `flake8` hook.
2021-03-20 11:13:59 -03:00
Bruno Oliveira
1637dc18d0 Merge pull request #638 from AizenGlobal/hotfix/master_to_main
[HOTFIX] rename pytest's branch(master to main)
2021-03-20 11:08:42 -03:00
baekdohyeop
f5342962ab [HOTFIX] rename pytest's branch(master to main) 2021-03-15 13:14:08 +09:00
Ronny Pfannschmidt
1189ae4b91 Merge pull request #624 from pytest-dev/fix-592-use-controller
prepare #592: replace master with controller where we can
2021-02-11 12:27:08 +01:00
Ronny Pfannschmidt
89c86af894 Merge pull request #627 from pytest-dev/release-2.2.1
release 2.2.1
2021-02-09 22:11:18 +01:00
Ronny Pfannschmidt
b239199061 release 2.2.1
Some checks failed
build / build (ubuntu-latest, 3.5, py35-pytestlatest) (push) Failing after 1s
build / build (ubuntu-latest, 3.6, py36-pytestlatest) (push) Failing after 1s
build / build (ubuntu-latest, 3.7, linting) (push) Failing after 1s
build / build (ubuntu-latest, 3.7, py37-pytestlatest) (push) Failing after 1s
build / build (ubuntu-latest, 3.8, py38-psutil) (push) Failing after 1s
build / build (ubuntu-latest, 3.8, py38-pytestlatest) (push) Failing after 1s
build / build (ubuntu-latest, 3.8, py38-pytestmaster) (push) Failing after 1s
build / build (ubuntu-latest, 3.9, py39-pytestlatest) (push) Failing after 1s
build / linting (push) Failing after 1s
build / build (windows-latest, 3.5, py35-pytestlatest) (push) Has been cancelled
build / build (windows-latest, 3.6, py36-pytestlatest) (push) Has been cancelled
build / build (windows-latest, 3.7, linting) (push) Has been cancelled
build / build (windows-latest, 3.7, py37-pytestlatest) (push) Has been cancelled
build / build (windows-latest, 3.8, py38-psutil) (push) Has been cancelled
build / build (windows-latest, 3.8, py38-pytestlatest) (push) Has been cancelled
build / build (windows-latest, 3.8, py38-pytestmaster) (push) Has been cancelled
build / build (windows-latest, 3.9, py39-pytestlatest) (push) Has been cancelled
build / deploy (push) Has been cancelled
2021-02-09 21:55:43 +01:00
Ronny Pfannschmidt
9e4e8b74e0 Apply suggestions from code review
Co-authored-by: Bruno Oliveira <nicoddemus@gmail.com>
2021-02-09 21:51:30 +01:00
Ronny Pfannschmidt
166bdb4103 prepare #592: replace master with controller where we can 2021-02-07 22:08:22 +01:00
Ronny Pfannschmidt
e4bcbbee78 Merge pull request #618 from staticdev/add-python-39-docs
Add python 3.9 to classifiers
2021-02-07 20:58:07 +01:00
Ronny Pfannschmidt
f6308cad01 Merge pull request #623 from pytest-dev/pytest-fspath-deprecation-prep
prepare for Node.fspath deprecation
2021-02-07 20:57:16 +01:00
Ronny Pfannschmidt
aa89c42ad7 prepare for Node.fspath deprecation
calculate topdir based on config.rootpath/rootdir

addresses pytest-dev/pytest#8251
2021-02-07 20:44:31 +01:00
staticdev
40fa7b091d Add python 3.9 to classifiers 2021-01-19 20:39:44 +01:00
Michał Górny
ad99d943de Disable pytest-services plugin in test to avoid worker_id conflict (#612)
Co-authored-by: Bruno Oliveira <nicoddemus@gmail.com>
2020-12-15 09:07:13 -03:00
Bruno Oliveira
0d2480f7e3 Merge pull request #610 from nicoddemus/release-2.2.0
Release 2.2.0
2020-12-14 09:15:48 -03:00
Bruno Oliveira
8f90fb73db Release 2.2.0
Some checks failed
build / build (ubuntu-latest, 3.5, py35-pytestlatest) (push) Failing after 1s
build / build (ubuntu-latest, 3.6, py36-pytestlatest) (push) Failing after 2s
build / build (ubuntu-latest, 3.7, linting) (push) Failing after 1s
build / build (ubuntu-latest, 3.7, py37-pytestlatest) (push) Failing after 1s
build / build (ubuntu-latest, 3.8, py38-psutil) (push) Failing after 1s
build / build (ubuntu-latest, 3.8, py38-pytestlatest) (push) Failing after 1s
build / build (ubuntu-latest, 3.8, py38-pytestmaster) (push) Failing after 1s
build / build (ubuntu-latest, 3.9, py39-pytestlatest) (push) Failing after 1s
build / linting (push) Failing after 1s
build / build (windows-latest, 3.5, py35-pytestlatest) (push) Has been cancelled
build / build (windows-latest, 3.6, py36-pytestlatest) (push) Has been cancelled
build / build (windows-latest, 3.7, linting) (push) Has been cancelled
build / build (windows-latest, 3.7, py37-pytestlatest) (push) Has been cancelled
build / build (windows-latest, 3.8, py38-psutil) (push) Has been cancelled
build / build (windows-latest, 3.8, py38-pytestlatest) (push) Has been cancelled
build / build (windows-latest, 3.8, py38-pytestmaster) (push) Has been cancelled
build / build (windows-latest, 3.9, py39-pytestlatest) (push) Has been cancelled
build / deploy (push) Has been cancelled
2020-12-14 09:07:49 -03:00
Bruno Oliveira
c5fadcd734 Merge pull request #608 from nicoddemus/better-errors
Propagate internal errors to the master node
2020-12-14 09:04:11 -03:00
Bruno Oliveira
16d636391b Propagate internal errors to the master node
This should help users diagnose internal errors in workers like
exceptions from hooks or in pytest itself.
2020-12-13 20:05:31 -03:00
Bruno Oliveira
948f13730d Merge pull request #609 from nicoddemus/gh-actions 2020-12-13 19:58:48 -03:00
Bruno Oliveira
db896a9d33 tox linting env expects Python 3.7 2020-12-12 12:06:33 -03:00
Bruno Oliveira
cf45eab977 Use GHA badge in README 2020-12-12 12:02:17 -03:00
Bruno Oliveira
00b0464d4b Remove travis and appveyor configs 2020-12-12 12:01:16 -03:00
Bruno Oliveira
b3a1bf3c2e Fix test_warning_captured_deprecated_in_pytest_6
The test was failing because *other* warnings were being triggered on the
master node. This makes the test more reliable by checking only that
the warning from the worker node is not emitted.

Fix #601
2020-12-12 11:58:41 -03:00
Bruno Oliveira
3fb369c8ac Fix linting 2020-12-12 11:52:47 -03:00
Bruno Oliveira
16694a978d Pass PRE_COMMIT_HOME to reuse local cache 2020-12-12 11:52:37 -03:00
Bruno Oliveira
2dcb40799f Setup GitHub Actions 2020-12-12 11:45:40 -03:00
Bruno Oliveira
14bafa6dd5 Rename HOWTORELEASE.rst to RELEASING.rst
To comply with pytest's nomenclature.
2020-09-22 09:45:02 -03:00
Bruno Oliveira
b51286aaa2 Merge pull request #599 from yili1992/master 2020-09-12 07:56:49 -03:00
易陆
03ddee6620 docs: fix invalid url 2020-09-10 10:16:16 +08:00
易陆
e03566b7d6 docs: fix invalid url 2020-09-10 10:14:37 +08:00
Bruno Oliveira
46cf204c64 Merge pull request #591 from nicoddemus/release-2.1.0
Release 2.1.0
2020-08-25 09:40:01 -03:00
Bruno Oliveira
5b7eeab7dc Release 2.1.0 2020-08-25 09:01:37 -03:00
Bruno Oliveira
9fd3b66292 Merge pull request #590 from nicoddemus/auto-hook 2020-08-25 09:00:38 -03:00
Bruno Oliveira
fe48256f1a Remove universal wheel setting: pytest-xdist is Python 3 only 2020-08-25 08:45:45 -03:00
Bruno Oliveira
607d8288d4 Add psutil extra and introduce pytest_xdist_auto_num_workers hook
This makes using psutil optional and opens up the possibility of customization
through the pytest_xdist_auto_num_workers hook, making things like #477 possible.

Fix #585
2020-08-25 08:38:28 -03:00
Bruno Oliveira
e469fc7b80 Revert "Merge pull request #560 from utapyngo/logical-cpu-count"
This reverts commit 0094b29a14, reversing
changes made to c6255faad4.
2020-08-24 14:50:36 -03:00
Bruno Oliveira
7caf7437e3 Merge pull request #588 from pelmini/fix_readme 2020-08-24 14:46:33 -03:00
pelmini
457386f837 Fixed typo in README 2020-08-23 17:15:02 -07:00
Bruno Oliveira
1d08799c82 Merge pull request #582 from nicoddemus/fix-docs
Fix session-scoped fixtures example in README
2020-08-15 07:56:28 -03:00
Bruno Oliveira
ac5b9956bb Fix session-scoped fixtures example in README 2020-08-15 07:43:11 -03:00
Bruno Oliveira
995b34aadc Merge pull request #575 from nicoddemus/release-2.0.0 2020-08-14 11:34:48 -03:00
Bruno Oliveira
43dad454c6 Prepare release 2.0.0 2020-08-12 17:28:27 -03:00
Christine M
c211d247a7 Noisy rsync (#473)
Co-authored-by: Bruno Oliveira <nicoddemus@gmail.com>
2020-08-12 17:23:03 -03:00
Thomas Grainger
2b8b8408bd run pyupgrade in ci (#573)
Co-authored-by: Bruno Oliveira <nicoddemus@gmail.com>
2020-08-06 13:03:06 -03:00
Bruno Oliveira
30606257fc Merge pull request #572 from pytest-dev/pytest6
Remove compat for pytest < 6
2020-08-06 09:12:58 -03:00
Zac-HD
d153e0a4c4 Remove compat for pytest < 6 2020-08-06 15:48:33 +10:00
Sylvain Marié
ba8398478e New methods is_xdist_worker, is_xdist_master, get_xdist_worker_id. (#505)
Co-authored-by: Sylvain MARIE <sylvain.marie@se.com>
Co-authored-by: Zac Hatfield-Dodds <Zac-HD@users.noreply.github.com>
Co-authored-by: Bruno Oliveira <nicoddemus@gmail.com>
2020-08-05 20:41:54 -03:00
Bruno Oliveira
abc1ae0e02 Merge pull request #571 from nicoddemus/remove-pytest-logwarning
Drop features from old pytest versions
2020-08-05 20:27:57 -03:00
Bruno Oliveira
047816f33a Drop features from old pytest versions 2020-08-05 19:55:56 -03:00
Bruno Oliveira
56a852665d Merge pull request #570 from nicoddemus/fix-config-init 2020-08-05 19:44:55 -03:00
Bruno Oliveira
55838a126d Remove unnecessary skipif and changelog 2020-08-05 19:24:53 -03:00
Michał Górny
bcfbf9f94b tox.ini: Extend testing to py39 2020-08-05 19:21:27 -03:00
Michał Górny
94bdd18a03 Fix skipping test_config_initialization for pytest<5
Fix test_config_initialization to be correctly skipped on pytest<5,
by explicitly checking pytest version (idea copied from
test_looponfail_removed_test).  The current conditions are insufficient
-- the outer check wrongly assumes pytest>=5 will always be used
on Python 3 (which is not true if you need the same version to support
both Python 2 and Python 3), and the inner condition apparently
wrongly assuming that invocation_params attribute is not present
in pytest-4.
2020-08-05 19:20:14 -03:00
Bruno Oliveira
fe459ba716 Merge pull request #541 from pytest-dev/emancipation 2020-08-05 19:18:02 -03:00
Bruno Oliveira
2f014c39df Minor changelog adjustment 2020-08-05 19:02:50 -03:00
Bruno Oliveira
e5cdb771f7 Add CHANGELOG about dropping Python 2.7
Fix #569
2020-08-05 19:01:57 -03:00
Bruno Oliveira
bdf66f2c8e Add CHANGELOG about requiring pytest 6.0+ 2020-08-05 18:57:25 -03:00
Hugo
62bc333e3e Fix tests 2020-08-03 22:52:35 +10:00
Hugo
dddac7c7e9 Remove six library 2020-08-03 22:52:18 +10:00
Hugo
9decca0023 Drop support for EOL Python 2 2020-08-03 22:52:03 +10:00
Hugo
22ae7ff7af Upgrade Python syntax with pyupgrade --py3-plus 2020-08-03 22:52:03 +10:00
Hugo
6f315356be Set py35 as target version for Black 2020-08-03 22:52:03 +10:00
Zac-HD
de3e54fd27 Finish removal of "slave" terminology
See issue #234.  It's time.
2020-08-03 22:51:42 +10:00
Zac-HD
f23c717a36 Delete old issues file 2020-08-03 22:50:13 +10:00
Zac Hatfield-Dodds
0094b29a14 Merge pull request #560 from utapyngo/logical-cpu-count 2020-08-03 22:49:09 +10:00
Ivan Zaikin
e528a42ddc Support OpenBSD and NetBSD 2020-07-28 08:55:32 +07:00
Bruno Oliveira
c6255faad4 Merge pull request #564 from nicoddemus/release-1.34.0
Release 1.34.0
2020-07-27 20:09:04 -03:00
Bruno Oliveira
7bf654775d Release 1.34.0 2020-07-27 19:36:42 -03:00
Adam Johnson
b524317863 Make "--pdb" imply "--dist no"
Co-authored-by: Bruno Oliveira <nicoddemus@gmail.com>
2020-07-24 20:07:42 -03:00
Daniel Hahler
467ac33d0b tests: check that PYTEST_ADDOPTS is not processed twice 2020-07-24 19:04:16 -03:00
Bruno Oliveira
fac0c14e28 Merge pull request #531 from mgorny/timeout 2020-07-24 15:47:20 -03:00
Adam Johnson
82730e5c6c Improve description of testing with multiple CPU's
Co-authored-by: Bruno Oliveira <nicoddemus@gmail.com>
2020-07-24 15:46:49 -03:00
Bruno Oliveira
78eaa4c0e2 Merge pull request #563 from nicoddemus/pytest-warning-captured-562
Do not trigger pytest_warning_captured in pytest 6.0+
2020-07-24 15:40:19 -03:00
Bruno Oliveira
18b18870a8 Do not trigger pytest_warning_captured in pytest 6.0+
Fix #562
2020-07-24 15:19:07 -03:00
Ivan Zaikin
3312dcde61 Apply black 2020-07-23 10:42:18 +07:00
Ivan Zaikin
1c85f1817a Add changelog 2020-07-23 10:42:04 +07:00
Ivan Zaikin
77e5fdfef9 Use psutil.cpu_count(logical=False) in auto_detect_cpus() 2020-07-23 10:18:43 +07:00
Bruno Oliveira
2fc21fddbb Merge pull request #558 from nicoddemus/fix-rsync-dirs
Fix rsyncdirs usage with pytest 6.0
2020-07-13 15:09:48 -03:00
Bruno Oliveira
443dc21061 Fix rsyncdirs usage with pytest 6.0
Fix pytest-dev/pytest#7487
2020-07-13 12:09:02 -03:00
Bruno Oliveira
727c41e957 Merge pull request #556 from nicoddemus/release-1.33.0 2020-07-09 20:47:50 -03:00
Bruno Oliveira
7396ffb7e8 Release 1.33.0 2020-07-09 20:20:02 -03:00
Bruno Oliveira
fbfe3b9a91 Add missing changelog for #548 2020-07-09 20:19:17 -03:00
Bruno Oliveira
075a74202b Add missing changelog for #554 2020-07-09 20:14:37 -03:00
Bruno Oliveira
b8e1e63c7a Merge pull request #554 from nicoddemus/fix-compat-pytest-master 2020-07-09 16:49:50 -03:00
Bruno Oliveira
bc0866f216 Merge pull request #548 from swt2c/exclude_scm_ci_files
Exclude SCM and CI files from distribution
2020-07-08 18:12:33 -03:00
Bruno Oliveira
b25757199c Support new `pytest_warning_recorded` hook from pytest 6.0 2020-07-08 17:51:45 -03:00
Bruno Oliveira
77a1db27cd Fix compatibility with pytest master
Fix the autouse fixture which diverts atexit.register calls to support the correct signature.
2020-07-08 15:58:31 -03:00
Bruno Oliveira
f5021313ec Merge pull request #545 from lgeiger/list-comprehensions
Prefer list comprehensions over list.append()
2020-07-08 15:44:25 -03:00
Scott Talbert
925829cb72 Exclude SCM and CI files from distribution
These files don't make sense outside of the git respository.
2020-06-26 12:02:03 -04:00
Lukas Geiger
311cbfabfb Prefer list comprehensions over list.append() 2020-06-21 01:00:02 +02:00
Bruno Oliveira
e48491d4fd Fix linting 2020-05-03 09:38:57 -03:00
Michał Górny
27519e8018 Increase pexpect timeouts to prevent tests from failing 2020-05-03 11:24:03 +02:00
Bruno Oliveira
59caed8e2f Merge pull request #530 from pytest-dev/release-1.32.0 2020-05-02 20:50:47 -03:00
Sven-Hendrik Haase
d3e1a52caa Prepare version 1.32.0 2020-05-03 01:00:40 +02:00
Bruno Oliveira
905eb3244d Merge pull request #528 from ashokdelphia/fix-acessing-typo-in-readme 2020-05-02 11:41:18 -03:00
Ashok Argent-Katwala
b4fb97a6e0 Fix typo in readme. 2020-05-02 11:40:23 -03:00
Sven-Hendrik Haase
e8bbb4db2a Merge pull request #525 from pytest-dev/524-add-global-session-uuid
Add global testrun uid
2020-05-01 19:36:53 +02:00
Sven-Hendrik Haase
069f959690 Fix docs format 2020-05-01 18:56:09 +02:00
Sven-Hendrik Haase
f56596bcdc Merge branch 'master' into 524-add-global-session-uuid 2020-05-01 16:56:23 +02:00
Sven-Hendrik Haase
ec13ceaa0c Merge pull request #527 from pytest-dev/ignore-python38-failure
Ignore Python 3.8 specific failure in pytest for now
2020-05-01 16:54:21 +02:00
Sven-Hendrik Haase
8714a99d51 Ignore Python 3.8 specific failure in pytest for now 2020-04-30 21:08:21 +02:00
Sven-Hendrik Haase
2183d741bb Add global session uuid (fixes #524) 2020-04-30 19:43:16 +02:00
Daniel Hahler
453cbd88dd tox/ci: remove testing with pytest's features branch (#509)
pytest is using the master branch now only.
2020-03-31 11:11:05 +02:00
Bruno Oliveira
bea6f66174 Merge pull request #475 from hugovk/rm-3.4
Drop support for EOL Python 3.4
2020-03-28 23:01:12 -03:00
Hugo van Kemenade
3a470043c9 Merge branch 'master' into rm-3.4 2020-01-26 21:05:51 +02:00
Bruno Oliveira
6fd5b56491 Merge pull request #495 from nicoddemus/release-1.31.0
Release 1.31.0
2019-12-19 22:45:30 -03:00
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
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
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
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
56 changed files with 4077 additions and 2294 deletions

33
.github/workflows/deploy.yml vendored Normal file
View File

@@ -0,0 +1,33 @@
name: deploy
on:
push:
tags:
- "v*"
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
# Needed to fetch tags, which are required by setuptools-scm.
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: "3.10"
- name: Install build
run: |
python -m pip install --upgrade pip
pip install build
- name: Build package
run: |
python -m build
- name: Publish package to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
user: __token__
password: ${{ secrets.pypi_token }}

67
.github/workflows/test.yml vendored Normal file
View File

@@ -0,0 +1,67 @@
name: test
on:
push:
branches:
- "*"
pull_request:
branches:
- "*"
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
tox_env:
- "py37-pytestlatest"
- "py38-pytestlatest"
- "py39-pytestlatest"
- "py310-pytestlatest"
- "py310-pytestmain"
- "py311-pytestlatest"
- "py311-pytestmain"
- "py310-psutil"
- "py310-setproctitle"
os: [ubuntu-latest, windows-latest]
include:
- tox_env: "py37-pytestlatest"
python: "3.7"
- tox_env: "py38-pytestlatest"
python: "3.8"
- tox_env: "py39-pytestlatest"
python: "3.9"
- tox_env: "py310-pytestlatest"
python: "3.10"
- tox_env: "py310-pytestmain"
python: "3.10"
- tox_env: "py311-pytestlatest"
python: "3.11"
- tox_env: "py311-pytestmain"
python: "3.11"
- tox_env: "py310-psutil"
python: "3.10"
- tox_env: "py310-setproctitle"
python: "3.10"
steps:
- uses: actions/checkout@v3
with:
# Needed to fetch tags, which are required by setuptools-scm.
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python }}
- name: Install tox
run: |
python -m pip install --upgrade pip
pip install tox
- name: Test
run: |
tox -e ${{ matrix.tox_env }}

2
.gitignore vendored
View File

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

View File

@@ -1,18 +1,35 @@
repos: repos:
- repo: https://github.com/ambv/black - repo: https://github.com/PyCQA/autoflake
rev: 18.6b4 rev: v2.0.0
hooks:
- id: autoflake
args: ["--in-place", "--remove-unused-variables", "--remove-all-unused-imports"]
- repo: https://github.com/psf/black
rev: 22.10.0
hooks: hooks:
- id: black - id: black
args: [--safe, --quiet] args: [--safe, --quiet, --target-version, py35]
language_version: python3.6 - repo: https://github.com/asottile/blacken-docs
rev: v1.12.1
hooks:
- id: blacken-docs
additional_dependencies: [black==20.8b1]
- repo: https://github.com/pre-commit/pre-commit-hooks - repo: https://github.com/pre-commit/pre-commit-hooks
rev: v1.3.0 rev: v4.4.0
hooks: hooks:
- id: trailing-whitespace - id: trailing-whitespace
- id: end-of-file-fixer - id: end-of-file-fixer
- id: check-yaml - id: check-yaml
- id: debug-statements - id: debug-statements
- repo: https://github.com/PyCQA/flake8
rev: 6.0.0
hooks:
- id: flake8 - id: flake8
- repo: https://github.com/asottile/pyupgrade
rev: v3.2.2
hooks:
- id: pyupgrade
args: [--py3-plus]
- repo: local - repo: local
hooks: hooks:
- id: rst - id: rst
@@ -21,4 +38,12 @@ repos:
files: ^(CHANGELOG.rst|HOWTORELEASE.rst|README.rst|changelog/.*)$ files: ^(CHANGELOG.rst|HOWTORELEASE.rst|README.rst|changelog/.*)$
language: python language: python
additional_dependencies: [pygments, restructuredtext_lint] additional_dependencies: [pygments, restructuredtext_lint]
python_version: python3.6 - repo: https://github.com/pre-commit/mirrors-mypy
rev: v0.991
hooks:
- id: mypy
files: ^(src/|testing/)
args: []
additional_dependencies:
- pytest>=6.2.0
- py>=1.10.0

View File

@@ -1,67 +0,0 @@
sudo: false
language: python
notifications:
irc:
channels:
- 'chat.freenode.net#pytest'
on_success: change
on_failure: change
skip_join: true
email:
- pytest-commit@python.org
install:
- pip install -U pip
- pip install tox setuptools_scm
script: tox
stages:
- baseline
- test
- name: deploy
if: repo = pytest-dev/pytest-xdist AND tag IS present
jobs:
include:
- stage: baseline
python: '3.6'
env: TOXENV=linting
- python: '3.6'
env: TOXENV=py36-pytestlatest
- python: '2.7'
env: TOXENV=py27-pytestlatest
- stage: test
python: "3.4"
env: TOXENV=py34-pytestlatest
- python: "3.5"
env: TOXENV=py35-pytestlatest
- python: "3.7"
env: TOXENV=py37-pytestlatest
sudo: required
dist: xenial
- python: "2.7"
env: TOXENV=py27-pytestmaster
- python: "2.7"
env: TOXENV=py27-pytestfeatures
- python: "3.6"
env: TOXENV=py36-pytestmaster
- python: "3.6"
env: TOXENV=py36-pytestfeatures
- stage: deploy
python: '3.6'
env:
install: pip install -U setuptools setuptools_scm
script: skip
deploy:
provider: pypi
user: ronny
distributions: sdist bdist_wheel
skip_upload_docs: true
password:
secure: cxmSDho5d+PYKEM4ZCg8ms1P4lzhYkrw6fEOm2HtTcsuCyY6aZMSgImWAnEYbJHSkdzgcxlXK9UKJ9B0YenXmBCkAr7UjdnpNXNmkySr0sYzlH/sfqt/dDATCHFaRKxnkOSOVywaDYhT9n8YudbXI77pXwD12i/CeSSJDbHhsu0JYUfAcb+D6YjRYoA2SEGCnzSzg+gDDfwXZx4ZiODCGLVwieNp1klCg88YROUE1BaYYNuUOONvfXX8+TWowbCF6ChH1WL/bZ49OStEYQNuYxZQZr4yClIqu9VJbchrU8j860K9ott2kkGTgfB/dDrQB/XncBubyIX9ikzCQAmmBXWAI3eyvWLPDk2Jz7kW2l2RT7syct80tCq3JhvQ1qdwr5ap7siocTLgnBW0tF4tkHSTFN3510fkc43npnp6FThebESQpnI24vqpwJ9hI/kW5mYi014Og2E/cpCXnz2XO8iZPDbqAMQpDsqEQoyhfGNgPTGp4K30TxRtwZBI5hHhDKnnR16fXtRgt1gYPvz/peUQvvpOm4JzIzGXPzluuutpnCBy75v5+oiwT3YRrLL/Meims9FtDDXL3qQubAE/ezIOOpm0N5XXV8DxIom8EN71yq5ab1tqhM+tBX7owRjy4FR4If2Q8feBdmTuh26DIQt/y+qSG8VkB9Sw/JCjc7c=
on:
tags: true
repo: pytest-dev/pytest-xdist

View File

@@ -1,3 +1,407 @@
pytest-xdist 3.1.0 (2022-12-01)
===============================
Features
--------
- `#789 <https://github.com/pytest-dev/pytest-xdist/issues/789>`_: Users can now set a default distribution mode in their configuration file:
.. code-block:: ini
[pytest]
addopts = --dist loadscope
- `#842 <https://github.com/pytest-dev/pytest-xdist/issues/842>`_: Python 3.11 is now officially supported.
Removals
--------
- `#842 <https://github.com/pytest-dev/pytest-xdist/issues/842>`_: Python 3.6 is no longer supported.
pytest-xdist 3.0.2 (2022-10-25)
===============================
Bug Fixes
---------
- `#813 <https://github.com/pytest-dev/pytest-xdist/issues/813>`_: Cancel shutdown when a crashed worker is restarted.
Deprecations
------------
- `#825 <https://github.com/pytest-dev/pytest-xdist/issues/825>`_: The ``--rsyncdir`` command line argument and ``rsyncdirs`` config variable are deprecated.
The rsync feature will be removed in pytest-xdist 4.0.
- `#826 <https://github.com/pytest-dev/pytest-xdist/issues/826>`_: The ``--looponfail`` command line argument and ``looponfailroots`` config variable are deprecated.
The loop-on-fail feature will be removed in pytest-xdist 4.0.
Improved Documentation
----------------------
- `#791 <https://github.com/pytest-dev/pytest-xdist/issues/791>`_: Document the ``pytest_xdist_auto_num_workers`` hook.
- `#796 <https://github.com/pytest-dev/pytest-xdist/issues/796>`_: Added known limitations section to documentation.
- `#829 <https://github.com/pytest-dev/pytest-xdist/issues/829>`_: Document the ``-n logical`` option.
Features
--------
- `#792 <https://github.com/pytest-dev/pytest-xdist/issues/792>`_: The environment variable ``PYTEST_XDIST_AUTO_NUM_WORKERS`` can now be used to
specify the default for ``-n auto`` and ``-n logical``.
- `#812 <https://github.com/pytest-dev/pytest-xdist/issues/812>`_: Partially restore old initial batch distribution algorithm in ``LoadScheduling``.
pytest orders tests for optimal sequential execution - i. e. avoiding
unnecessary setup and teardown of fixtures. So executing tests in consecutive
chunks is important for optimal performance.
In v1.14, initial test distribution in ``LoadScheduling`` was changed to
round-robin, optimized for the corner case, when the number of tests is less
than ``2 * number of nodes``. At the same time, it became worse for all other
cases.
For example: if some tests use some "heavy" fixture, and these tests fit into
the initial batch, with round-robin distribution the fixture will be created
``min(n_tests, n_workers)`` times, no matter how many other tests there are.
With the old algorithm (before v1.14), if there are enough tests not using
the fixture, the fixture was created only once.
So restore the old behavior for typical cases where the number of tests is
much greater than the number of workers (or, strictly speaking, when there
are at least 2 tests for every node).
Removals
--------
- `#468 <https://github.com/pytest-dev/pytest-xdist/issues/468>`_: The ``--boxed`` command-line option has been removed. If you still need this functionality, install `pytest-forked <https://pypi.org/project/pytest-forked>`__ separately.
Trivial Changes
---------------
- `#468 <https://github.com/pytest-dev/pytest-xdist/issues/468>`_: The ``py`` dependency has been dropped.
- `#822 <https://github.com/pytest-dev/pytest-xdist/issues/822>`_: Replace internal usage of ``py.log`` with a custom solution (but with the same interface).
- `#823 <https://github.com/pytest-dev/pytest-xdist/issues/823>`_: Remove usage of ``py._pydir`` as an rsync candidate.
- `#824 <https://github.com/pytest-dev/pytest-xdist/issues/824>`_: Replace internal usages of ``py.path.local`` by ``pathlib.Path``.
pytest-xdist 2.5.0 (2021-12-10)
===============================
Deprecations and Removals
-------------------------
- `#468 <https://github.com/pytest-dev/pytest-xdist/issues/468>`_: The ``--boxed`` command line argument is deprecated. Install `pytest-forked <https://pypi.org/project/pytest-forked>`__ and use ``--forked`` instead. pytest-xdist 3.0.0 will remove the ``--boxed`` argument and ``pytest-forked`` dependency.
Features
--------
- `#722 <https://github.com/pytest-dev/pytest-xdist/issues/722>`_: Full compatibility with pytest 7 - no deprecation warnings or use of legacy features.
- `#733 <https://github.com/pytest-dev/pytest-xdist/issues/733>`_: New ``--dist=loadgroup`` option, which ensures all tests marked with ``@pytest.mark.xdist_group`` run in the same session/worker. Other tests run distributed as in ``--dist=load``.
Trivial Changes
---------------
- `#708 <https://github.com/pytest-dev/pytest-xdist/issues/708>`_: Use ``@pytest.hookspec`` decorator to declare hook options in ``newhooks.py`` to avoid warnings in ``pytest 7.0``.
- `#719 <https://github.com/pytest-dev/pytest-xdist/issues/719>`_: Use up-to-date ``setup.cfg``/``pyproject.toml`` packaging setup.
- `#720 <https://github.com/pytest-dev/pytest-xdist/issues/720>`_: Require pytest>=6.2.0.
- `#721 <https://github.com/pytest-dev/pytest-xdist/issues/721>`_: Started using type annotations and mypy checking internally. The types are incomplete and not published.
pytest-xdist 2.4.0 (2021-09-20)
===============================
Features
--------
- `#696 <https://github.com/pytest-dev/pytest-xdist/issues/696>`_: On Linux, the process title now changes to indicate the current worker state (running/idle).
Depends on the `setproctitle <https://pypi.org/project/setproctitle/>`__ package, which can be installed with ``pip install pytest-xdist[setproctitle]``.
- `#704 <https://github.com/pytest-dev/pytest-xdist/issues/704>`_: Add support for Python 3.10.
pytest-xdist 2.3.0 (2021-06-16)
===============================
Deprecations and Removals
-------------------------
- `#654 <https://github.com/pytest-dev/pytest-xdist/issues/654>`_: Python 3.5 is no longer supported.
Features
--------
- `#646 <https://github.com/pytest-dev/pytest-xdist/issues/646>`_: Add ``--numprocesses=logical`` flag, which automatically uses the number of logical CPUs available, instead of physical CPUs with ``auto``.
This is very useful for test suites which are not CPU-bound.
- `#650 <https://github.com/pytest-dev/pytest-xdist/issues/650>`_: Added new ``pytest_handlecrashitem`` hook to allow handling and rescheduling crashed items.
Bug Fixes
---------
- `#421 <https://github.com/pytest-dev/pytest-xdist/issues/421>`_: Copy the parent process sys.path into local workers, to work around execnet's python -c adding the current directory to sys.path.
- `#638 <https://github.com/pytest-dev/pytest-xdist/issues/638>`_: Fix issue caused by changing the branch name of the pytest repository.
Trivial Changes
---------------
- `#592 <https://github.com/pytest-dev/pytest-xdist/issues/592>`_: Replace master with controller where ever possible.
- `#643 <https://github.com/pytest-dev/pytest-xdist/issues/643>`_: Use 'main' to refer to pytest default branch in tox env names.
pytest-xdist 2.2.1 (2021-02-09)
===============================
Bug Fixes
---------
- `#623 <https://github.com/pytest-dev/pytest-xdist/issues/623>`_: Gracefully handle the pending deprecation of Node.fspath by using config.rootpath for topdir.
pytest-xdist 2.2.0 (2020-12-14)
===============================
Features
--------
- `#608 <https://github.com/pytest-dev/pytest-xdist/issues/608>`_: Internal errors in workers are now propagated to the master node.
pytest-xdist 2.1.0 (2020-08-25)
===============================
Features
--------
- `#585 <https://github.com/pytest-dev/pytest-xdist/issues/585>`_: New ``pytest_xdist_auto_num_workers`` hook can be implemented by plugins or ``conftest.py`` files to control the number of workers when ``--numprocesses=auto`` is given in the command-line.
Trivial Changes
---------------
- `#585 <https://github.com/pytest-dev/pytest-xdist/issues/585>`_: ``psutil`` has proven to make ``pytest-xdist`` installation in certain platforms and containers problematic, so to use it for automatic number of CPUs detection users need to install the ``psutil`` extra::
pip install pytest-xdist[psutil]
pytest-xdist 2.0.0 (2020-08-12)
===============================
Deprecations and Removals
-------------------------
- `#541 <https://github.com/pytest-dev/pytest-xdist/issues/541>`_: Drop backward-compatibility "slave" aliases related to worker nodes. We deliberately moved away from this terminology years ago, and it seems like the right time to finish the deprecation and removal process.
- `#569 <https://github.com/pytest-dev/pytest-xdist/issues/569>`_: ``pytest-xdist`` no longer supports Python 2.7.
Features
--------
- `#504 <https://github.com/pytest-dev/pytest-xdist/issues/504>`_: New functions ``xdist.is_xdist_worker``, ``xdist.is_xdist_master``, ``xdist.get_xdist_worker_id``, to easily identify the current node.
Bug Fixes
---------
- `#471 <https://github.com/pytest-dev/pytest-xdist/issues/471>`_: Fix issue with Rsync reporting in quiet mode.
- `#553 <https://github.com/pytest-dev/pytest-xdist/issues/553>`_: When using ``-n auto``, count the number of physical CPU cores instead of logical ones.
Trivial Changes
---------------
- `#541 <https://github.com/pytest-dev/pytest-xdist/issues/541>`_: ``pytest-xdist`` now requires ``pytest>=6.0``.
pytest-xdist 1.34.0 (2020-07-27)
================================
Features
--------
- `#549 <https://github.com/pytest-dev/pytest-xdist/issues/549>`_: Make ``--pdb`` imply ``--dist no``, as the two options cannot really work together at the moment.
Bug Fixes
---------
- `#478 <https://github.com/pytest-dev/pytest-xdist/issues/478>`_: Fix regression with duplicated arguments via $PYTEST_ADDOPTS in 1.30.0.
- `#558 <https://github.com/pytest-dev/pytest-xdist/issues/558>`_: Fix ``rsyncdirs`` usage with pytest 6.0.
- `#562 <https://github.com/pytest-dev/pytest-xdist/issues/562>`_: Do not trigger the deprecated ``pytest_warning_captured`` in pytest 6.0+.
pytest-xdist 1.33.0 (2020-07-09)
================================
Features
--------
- `#554 <https://github.com/pytest-dev/pytest-xdist/issues/554>`_: Fix warnings support for upcoming pytest 6.0.
Trivial Changes
---------------
- `#548 <https://github.com/pytest-dev/pytest-xdist/issues/548>`_: SCM and CI files are no longer included in the source distribution.
pytest-xdist 1.32.0 (2020-05-03)
================================
Deprecations and Removals
-------------------------
- `#475 <https://github.com/pytest-dev/pytest-xdist/issues/475>`_: Drop support for EOL Python 3.4.
Features
--------
- `#524 <https://github.com/pytest-dev/pytest-xdist/issues/524>`_: Add `testrun_uid` fixture. This is a shared value that uniquely identifies a test run among all workers.
This also adds a `PYTEST_XDIST_TESTRUNUID` environment variable that is accessible within a test as well as a command line option `--testrunuid` to manually set the value from outside.
pytest-xdist 1.31.0 (2019-12-19)
================================
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) pytest-xdist 1.25.0 (2018-12-12)
================================ ================================
@@ -329,17 +733,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

@@ -1,31 +0,0 @@
next release critical
-----------------------------------------------
tag: bug
miserably fails: --dist=each --tx popen --tx socket=...
rename / hooks
-----------------------------------------------
tag: bug
node -> slave
transition for hooks?
configure_node -> configure_slave
allow to remotely run xdist tests with xdist
-----------------------------------------------
tag: feature
allow to run xdist own tests using its own mechanism.
currently this doesn't work because the remote side
has no pytest plugin. How to configure/do
register "xdist.plugin" on the remote side?
see to avoid any "from _pytest" internal imports
-----------------------------------------------
tag: feature
currently tests and even xdist core code imports
names from the internal _pytest namespace.
See to avoid it.

5
MANIFEST.in Normal file
View File

@@ -0,0 +1,5 @@
exclude .appveyor.yml
exclude .gitignore
exclude .pre-commit-config.yaml
exclude .travis.yml
prune .github

View File

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

View File

@@ -1,4 +1,6 @@
============
pytest-xdist
============
.. image:: http://img.shields.io/pypi/v/pytest-xdist.svg .. image:: http://img.shields.io/pypi/v/pytest-xdist.svg
:alt: PyPI version :alt: PyPI version
@@ -11,259 +13,21 @@
:alt: Python versions :alt: Python versions
:target: https://pypi.python.org/pypi/pytest-xdist :target: https://pypi.python.org/pypi/pytest-xdist
.. image:: https://travis-ci.org/pytest-dev/pytest-xdist.svg?branch=master .. image:: https://github.com/pytest-dev/pytest-xdist/workflows/test/badge.svg
:alt: Travis CI build status :target: https://github.com/pytest-dev/pytest-xdist/actions
:target: https://travis-ci.org/pytest-dev/pytest-xdist
.. image:: https://ci.appveyor.com/api/projects/status/56eq1a1avd4sdd7e/branch/master?svg=true
:alt: AppVeyor build status
:target: https://ci.appveyor.com/project/pytestbot/pytest-xdist
.. image:: https://img.shields.io/badge/code%20style-black-000000.svg .. image:: https://img.shields.io/badge/code%20style-black-000000.svg
:target: https://github.com/ambv/black :target: https://github.com/ambv/black
xdist: pytest distributed testing plugin The `pytest-xdist`_ plugin extends pytest with new test execution modes, the most used being distributing
======================================== tests across multiple CPUs to speed up test execution::
The `pytest-xdist`_ plugin extends pytest with some unique pytest -n auto
test execution modes:
* test run parallelization_: if you have multiple CPUs or hosts you can use With this call, pytest will spawn a number of workers processes equal to the number of available CPUs, and distribute
those for a combined test run. This allows to speed up the tests randomly across them.
development or to use special resources of `remote machines`_.
Documentation
=============
* ``--looponfail``: run your tests repeatedly in a subprocess. After each run Documentation is available at `Read The Docs <https://pytest-xdist.readthedocs.io>`__.
pytest waits until a file in your project changes and then re-runs
the previously failing tests. This is repeated until all tests pass
after which again a full run is performed.
* `Multi-Platform`_ coverage: you can specify different Python interpreters
or different platforms and run tests in parallel on all of them.
Before running tests remotely, ``pytest`` efficiently "rsyncs" your
program source code to the remote place. All test results
are reported back and displayed to your local terminal.
You may specify different Python versions and interpreters.
If you would like to know how pytest-xdist works under the covers, checkout
`OVERVIEW <https://github.com/pytest-dev/pytest-xdist/blob/master/OVERVIEW.md>`_.
Installation
------------
Install the plugin with::
pip install pytest-xdist
or use the package in develop/in-place mode with
a checkout of the `pytest-xdist repository`_ ::
pip install --editable .
.. _parallelization:
Speed up test runs by sending tests to multiple CPUs
----------------------------------------------------
To send tests to multiple CPUs, type::
pytest -n NUM
Especially for longer running tests or tests requiring
a lot of I/O this can lead to considerable speed ups. This option can
also be set to ``auto`` for automatic detection of the number of CPUs.
If a test crashes the interpreter, pytest-xdist will automatically restart
that worker and report the failure as usual. You can use the
``--max-worker-restart`` option to limit the number of workers that can
be restarted, or disable restarting altogether using ``--max-worker-restart=0``.
By default, the ``-n`` option will send pending tests to any worker that is available, without
any guaranteed order, but you can control this with these options:
* ``--dist=loadscope``: tests will be grouped by **module** for *test functions* and
by **class** for *test methods*, then each group will be sent to an available worker,
guaranteeing that all tests in a group run in the same process. This can be useful if you have
expensive module-level or class-level fixtures. Currently the groupings can't be customized,
with grouping by class takes priority over grouping by module.
This feature was added in version ``1.19``.
* ``--dist=loadfile``: tests will be grouped by file name, and then will be sent to an available
worker, guaranteeing that all tests in a group run in the same worker. This feature was added
in version ``1.21``.
Running tests in a Python subprocess
------------------------------------
To instantiate a python3.5 subprocess and send tests to it, you may type::
pytest -d --tx popen//python=python3.5
This will start a subprocess which is run with the ``python3.5``
Python interpreter, found in your system binary lookup path.
If you prefix the --tx option value like this::
--tx 3*popen//python=python3.5
then three subprocesses would be created and tests
will be load-balanced across these three processes.
.. _boxed:
Running tests in a boxed subprocess
-----------------------------------
This functionality has been moved to the
`pytest-forked <https://github.com/pytest-dev/pytest-forked>`_ plugin, but the ``--boxed`` option
is still kept for backward compatibility.
.. _`remote machines`:
Sending tests to remote SSH accounts
------------------------------------
Suppose you have a package ``mypkg`` which contains some
tests that you can successfully run locally. And you
have a ssh-reachable machine ``myhost``. Then
you can ad-hoc distribute your tests by typing::
pytest -d --tx ssh=myhostpopen --rsyncdir mypkg mypkg
This will synchronize your :code:`mypkg` package directory
to a remote ssh account and then locally collect tests
and send them to remote places for execution.
You can specify multiple :code:`--rsyncdir` directories
to be sent to the remote side.
.. note::
For pytest to collect and send tests correctly
you not only need to make sure all code and tests
directories are rsynced, but that any test (sub) directory
also has an :code:`__init__.py` file because internally
pytest references tests as a fully qualified python
module path. **You will otherwise get strange errors**
during setup of the remote side.
You can specify multiple :code:`--rsyncignore` glob patterns
to be ignored when file are sent to the remote side.
There are also internal ignores: :code:`.*, *.pyc, *.pyo, *~`
Those you cannot override using rsyncignore command-line or
ini-file option(s).
Sending tests to remote Socket Servers
--------------------------------------
Download the single-module `socketserver.py`_ Python program
and run it like this::
python socketserver.py
It will tell you that it starts listening on the default
port. You can now on your home machine specify this
new socket host with something like this::
pytest -d --tx socket=192.168.1.102:8888 --rsyncdir mypkg mypkg
.. _`atonce`:
.. _`Multi-Platform`:
Running tests on many platforms at once
---------------------------------------
The basic command to run tests on multiple platforms is::
pytest --dist=each --tx=spec1 --tx=spec2
If you specify a windows host, an OSX host and a Linux
environment this command will send each tests to all
platforms - and report back failures from all platforms
at once. The specifications strings use the `xspec syntax`_.
.. _`xspec syntax`: http://codespeak.net/execnet/basics.html#xspec
.. _`socketserver.py`: http://bitbucket.org/hpk42/execnet/raw/2af991418160/execnet/script/socketserver.py
.. _`execnet`: http://codespeak.net/execnet
Identifying the worker process during a test
--------------------------------------------
*New in version 1.15.*
If you need to determine the identity of a worker process in
a test or fixture, you may use the ``worker_id`` fixture to do so:
.. code-block:: python
@pytest.fixture()
def user_account(worker_id):
""" use a different account in each xdist worker """
return "account_%s" % worker_id
When ``xdist`` is disabled (running with ``-n0`` for example), then
``worker_id`` will return ``"master"``.
Additionally, worker processes have the following environment variables
defined:
* ``PYTEST_XDIST_WORKER``: the name of the worker, e.g., ``"gw2"``.
* ``PYTEST_XDIST_WORKER_COUNT``: the total number of workers in this session,
e.g., ``"4"`` when ``-n 4`` is given in the command-line.
The information about the worker_id in a test is stored in the ``TestReport`` as
well, under the ``worker_id`` attribute.
Specifying test exec environments in an ini file
------------------------------------------------
You can use pytest's ini file configuration to avoid typing common options.
You can for example make running with three subprocesses your default like this:
.. code-block:: ini
[pytest]
addopts = -n3
You can also add default environments like this:
.. code-block:: ini
[pytest]
addopts = --tx ssh=myhost//python=python3.5 --tx ssh=myhost//python=python3.6
and then just type::
pytest --dist=each
to run tests in each of the environments.
Specifying "rsync" dirs in an ini-file
--------------------------------------
In a ``tox.ini`` or ``setup.cfg`` file in your root project directory
you may specify directories to include or to exclude in synchronisation:
.. code-block:: ini
[pytest]
rsyncdirs = . mypkg helperpkg
rsyncignore = .hg
These directory specifications are relative to the directory
where the configuration file was found.
.. _`pytest-xdist`: http://pypi.python.org/pypi/pytest-xdist
.. _`pytest-xdist repository`: https://github.com/pytest-dev/pytest-xdist
.. _`pytest`: http://pytest.org

View File

@@ -41,3 +41,5 @@ To publish a new release ``X.Y.Z``, the steps are as follows:
$ git push git@github.com:pytest-dev/pytest-xdist.git v$VERSION $ git push git@github.com:pytest-dev/pytest-xdist.git v$VERSION
That will build the package and publish it on ``PyPI`` automatically. That will build the package and publish it on ``PyPI`` automatically.
#. Merge the release PR to `master`.

View File

@@ -1,24 +0,0 @@
environment:
matrix:
- TOXENV: "py27-pytestlatest"
- TOXENV: "py34-pytestlatest"
- TOXENV: "py35-pytestlatest"
- TOXENV: "py36-pytestlatest"
- TOXENV: "py37-pytestlatest"
- TOXENV: "py27-pytestmaster"
- TOXENV: "py36-pytestmaster"
- TOXENV: "py27-pytestfeatures"
- TOXENV: "py36-pytestfeatures"
install:
- C:\Python37\python -m pip install -U pip
- C:\Python37\python -m pip install -U tox setuptools_scm
build: false # Not a C# project, build stuff at the test step instead.
test_script:
- C:\Python37\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

1
docs/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
_build/

5
docs/changelog.rst Normal file
View File

@@ -0,0 +1,5 @@
=========
Changelog
=========
.. include:: ../CHANGELOG.rst

55
docs/conf.py Normal file
View File

@@ -0,0 +1,55 @@
# Configuration file for the Sphinx documentation builder.
#
# This file only contains a selection of the most common options. For a full
# list see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html
# -- Path setup --------------------------------------------------------------
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#
# import os
# import sys
# sys.path.insert(0, os.path.abspath('.'))
# -- Project information -----------------------------------------------------
project = "pytest-xdist"
copyright = "2022, holger krekel and contributors"
author = "holger krekel and contributors"
master_doc = "index"
# -- General configuration ---------------------------------------------------
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
"sphinx_rtd_theme",
"sphinx.ext.autodoc",
]
# Add any paths that contain templates here, relative to this directory.
templates_path = ["_templates"]
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This pattern also affects html_static_path and html_extra_path.
exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
# -- Options for HTML output -------------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
html_theme = "sphinx_rtd_theme"
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
# html_static_path = ['_static']

7
docs/crash.rst Normal file
View File

@@ -0,0 +1,7 @@
When tests crash
================
If a test crashes a worker, pytest-xdist will automatically restart that worker
and report the tests failure. You can use the ``--max-worker-restart`` option
to limit the number of worker restarts that are allowed, or disable restarting
altogether using ``--max-worker-restart 0``.

85
docs/distribution.rst Normal file
View File

@@ -0,0 +1,85 @@
.. _parallelization:
Running tests across multiple CPUs
==================================
To send tests to multiple CPUs, use the ``-n`` (or ``--numprocesses``) option::
pytest -n auto
This can lead to considerable speed ups, especially if your test suite takes a
noticeable amount of time.
With ``-n auto``, pytest-xdist will use as many processes as your computer
has CPU cores.
Use ``-n logical`` to use the number of *logical* CPU cores rather than
physical ones. This currently requires the ``psutil`` package to be installed;
if it is not, pytest-xdist will fall back to ``-n auto`` behavior.
Pass a number, e.g. ``-n 8``, to specify the number of processes explicitly.
To specify a different meaning for ``-n auto`` and ``-n logical`` for your
tests, you can:
* Set the environment variable ``PYTEST_XDIST_AUTO_NUM_WORKERS`` to the
desired number of processes.
* Implement the ``pytest_xdist_auto_num_workers``
`pytest hook <https://docs.pytest.org/en/latest/how-to/writing_plugins.html>`__
(a ``pytest_xdist_auto_num_workers(config)`` function in e.g. ``conftest.py``)
that returns the number of processes to use.
The hook can use ``config.option.numprocesses`` to determine if the user
asked for ``"auto"`` or ``"logical"``, and it can return ``None`` to fall
back to the default.
If both the hook and environment variable are specified, the hook takes
priority.
Parallelization can be configured further with these options:
* ``--maxprocesses=maxprocesses``: limit the maximum number of workers to
process the tests.
* ``--max-worker-restart``: maximum number of workers that can be restarted
when crashed (set to zero to disable this feature).
The test distribution algorithm is configured with the ``--dist`` command-line option:
.. _distribution modes:
* ``--dist load`` **(default)**: Sends pending tests to any worker that is
available, without any guaranteed order.
* ``--dist loadscope``: Tests are grouped by **module** for *test functions*
and by **class** for *test methods*. Groups are distributed to available
workers as whole units. This guarantees that all tests in a group run in the
same process. This can be useful if you have expensive module-level or
class-level fixtures. Grouping by class takes priority over grouping by
module.
* ``--dist loadfile``: Tests are grouped by their containing file. Groups are
distributed to available workers as whole units. This guarantees that all
tests in a file run in the same worker.
* ``--dist loadgroup``: Tests are grouped by the ``xdist_group`` mark. Groups are
distributed to available workers as whole units. This guarantees that all
tests with same ``xdist_group`` name run in the same worker.
.. code-block:: python
@pytest.mark.xdist_group(name="group1")
def test1():
pass
class TestA:
@pytest.mark.xdist_group("group1")
def test2():
pass
This will make sure ``test1`` and ``TestA::test2`` will run in the same worker.
Tests without the ``xdist_group`` mark are distributed normally as in the ``--dist=load`` mode.
* ``--dist no``: The normal pytest execution mode, runs one test at a time (no distribution at all).

90
docs/how-it-works.rst Normal file
View File

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

233
docs/how-to.rst Normal file
View File

@@ -0,0 +1,233 @@
How-tos
-------
This section show cases how to accomplish some specialized tasks with ``pytest-xdist``.
Identifying the worker process during a test
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
*New in version 1.15.*
If you need to determine the identity of a worker process in
a test or fixture, you may use the ``worker_id`` fixture to do so:
.. code-block:: python
@pytest.fixture()
def user_account(worker_id):
""" use a different account in each xdist worker """
return "account_%s" % worker_id
When ``xdist`` is disabled (running with ``-n0`` for example), then
``worker_id`` will return ``"master"``.
Worker processes also have the following environment variables
defined:
.. envvar:: PYTEST_XDIST_WORKER
The name of the worker, e.g., ``"gw2"``.
.. envvar:: PYTEST_XDIST_WORKER_COUNT
The total number of workers in this session, e.g., ``"4"`` when ``-n 4`` is given in the command-line.
The information about the worker_id in a test is stored in the ``TestReport`` as
well, under the ``worker_id`` attribute.
Since version 2.0, the following functions are also available in the ``xdist`` module:
.. autofunction:: xdist.is_xdist_worker
.. autofunction:: xdist.is_xdist_controller
.. autofunction:: xdist.is_xdist_master
.. autofunction:: xdist.get_xdist_worker_id
Identifying workers from the system environment
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
*New in version 2.4*
If the `setproctitle`_ package is installed, ``pytest-xdist`` will use it to
update the process title (command line) on its workers to show their current
state. The titles used are ``[pytest-xdist running] file.py/node::id`` and
``[pytest-xdist idle]``, visible in standard tools like ``ps`` and ``top`` on
Linux, Mac OS X and BSD systems. For Windows, please follow `setproctitle`_'s
pointer regarding the Process Explorer tool.
This is intended purely as an UX enhancement, e.g. to track down issues with
long-running or CPU intensive tests. Errors in changing the title are ignored
silently. Please try not to rely on the title format or title changes in
external scripts.
.. _`setproctitle`: https://pypi.org/project/setproctitle/
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:
.. envvar:: PYTEST_XDIST_TESTRUNUID
The unique id of the test run.
Accessing ``sys.argv`` from the controller node in workers
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
To access the ``sys.argv`` passed to the command-line of the controller node, use
``request.config.workerinput["mainargv"]``.
Specifying test exec environments in an ini file
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
You can use pytest's ini file configuration to avoid typing common options.
You can for example make running with three subprocesses your default like this:
.. code-block:: ini
[pytest]
addopts = -n3
You can also add default environments like this:
.. code-block:: ini
[pytest]
addopts = --tx ssh=myhost//python=python3.9 --tx ssh=myhost//python=python3.6
and then just type::
pytest --dist=each
to run tests in each of the environments.
Specifying "rsync" dirs in an ini-file
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
In a ``tox.ini`` or ``setup.cfg`` file in your root project directory
you may specify directories to include or to exclude in synchronisation:
.. code-block:: ini
[pytest]
rsyncdirs = . mypkg helperpkg
rsyncignore = .hg
These directory specifications are relative to the directory
where the configuration file was found.
.. _`pytest-xdist`: http://pypi.python.org/pypi/pytest-xdist
.. _`pytest-xdist repository`: https://github.com/pytest-dev/pytest-xdist
.. _`pytest`: http://pytest.org
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 worker_id == "master":
# 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.
Creating one log file for each worker
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
To create one log file for each worker with ``pytest-xdist``, you can leverage :envvar:`PYTEST_XDIST_WORKER`
to generate a unique filename for each worker.
Example:
.. code-block:: python
# content of conftest.py
def pytest_configure(config):
worker_id = os.environ.get("PYTEST_XDIST_WORKER")
if worker_id is not None:
log_file = config.getini("worker_log_file")
logging.basicConfig(
format=config.getini("log_file_format"),
filename=f"tests_{worker_id}.log",
level=config.getini("log_file_level"),
)
When running the tests with ``-n3``, for example, three files will be created in the current directory:
``tests_gw0.log``, ``tests_gw1.log`` and ``tests_gw2.log``.

63
docs/index.rst Normal file
View File

@@ -0,0 +1,63 @@
pytest-xdist
============
The `pytest-xdist`_ plugin extends pytest with new test execution modes, the most used being distributing
tests across multiple CPUs to speed up test execution::
pytest -n auto
With this call, pytest will spawn a number of workers processes equal to the number of available CPUs, and distribute
the tests randomly across them.
.. note::
Due to how pytest-xdist is implemented, the ``-s/--capture=no`` option does not work.
Installation
------------
Install the plugin with::
pip install pytest-xdist
To use ``psutil`` for detection of the number of CPUs available, install the ``psutil`` extra::
pip install pytest-xdist[psutil]
Features
--------
* Test run :ref:`parallelization`: tests can be executed across multiple CPUs or hosts.
This allows to speed up development or to use special resources of :ref:`remote machines`.
* ``--looponfail``: run your tests repeatedly in a subprocess. After each run
pytest waits until a file in your project changes and then re-runs
the previously failing tests. This is repeated until all tests pass
after which again a full run is performed (DEPRECATED).
* :ref:`Multi-Platform` coverage: you can specify different Python interpreters
or different platforms and run tests in parallel on all of them.
Before running tests remotely, ``pytest`` efficiently "rsyncs" your
program source code to the remote place.
You may specify different Python versions and interpreters. It does not
installs/synchronize dependencies however.
**Note**: this mode exists mostly for backward compatibility, as modern development
relies on continuous integration for multi-platform testing.
.. toctree::
:maxdepth: 2
:caption: Contents:
distribution
subprocess
remote
crash
how-to
how-it-works
known-limitations
changelog

View File

@@ -0,0 +1,64 @@
Known limitations
=================
pytest-xdist has some limitations that may be supported in pytest but can't be supported in pytest-xdist.
Order and amount of test must be consistent
-------------------------------------------
Is is not possible to have tests that differ in order or their amount across workers.
This is especially true with ``pytest.mark.parametrize``, when values are produced with sets or other unordered iterables/generators.
Example:
.. code-block:: python
import pytest
@pytest.mark.parametrize("param", {"a", "b"})
def test_pytest_parametrize_unordered(param):
pass
In the example above, the fact that ``set`` are not necessarily ordered can cause different workers
to collect tests in different order, which will throw an error.
Workarounds
~~~~~~~~~~~
A solution to this is to guarantee that the parametrized values have the same order.
Some solutions:
* Convert your sequence to a ``list``.
.. code-block:: python
import pytest
@pytest.mark.parametrize("param", ["a", "b"])
def test_pytest_parametrize_unordered(param):
pass
* Sort your sequence, guaranteeing order.
.. code-block:: python
import pytest
@pytest.mark.parametrize("param", sorted({"a", "b"}))
def test_pytest_parametrize_unordered(param):
pass
Output (stdout and stderr) from workers
---------------------------------------
The ``-s``/``--capture=no`` option is meant to disable pytest capture, so users can then see stdout and stderr output in the terminal from tests and application code in real time.
However this option does not work with ``pytest-xdist`` because `execnet <https://github.com/pytest-dev/execnet>`__ the underlying library used for communication between master and workers, does not support transferring stdout/stderr from workers.
Currenlty there are no plans ot support this in ``pytest-xdist``.

83
docs/remote.rst Normal file
View File

@@ -0,0 +1,83 @@
.. _`Multi-Platform`:
.. _`remote machines`:
Sending tests to remote SSH accounts
====================================
.. deprecated:: 3.0
.. warning::
This feature is deprecated because the support for ``rsync`` is faulty
in terms of reproducing the development environment in the remote
worker, and there is no clear solution moving forward.
This feature is scheduled to be removed in release 4.0, to let the team
focus on a smaller set of features.
Suppose you have a package ``mypkg`` which contains some
tests that you can successfully run locally. And you
have a ssh-reachable machine ``myhost``. Then
you can ad-hoc distribute your tests by typing::
pytest -d --rsyncdir mypkg --tx ssh=myhostpopen mypkg/tests/unit/test_something.py
This will synchronize your :code:`mypkg` package directory
to a remote ssh account and then locally collect tests
and send them to remote places for execution.
You can specify multiple :code:`--rsyncdir` directories
to be sent to the remote side.
.. note::
For pytest to collect and send tests correctly
you not only need to make sure all code and tests
directories are rsynced, but that any test (sub) directory
also has an :code:`__init__.py` file because internally
pytest references tests as a fully qualified python
module path. **You will otherwise get strange errors**
during setup of the remote side.
You can specify multiple :code:`--rsyncignore` glob patterns
to be ignored when file are sent to the remote side.
There are also internal ignores: :code:`.*, *.pyc, *.pyo, *~`
Those you cannot override using rsyncignore command-line or
ini-file option(s).
Sending tests to remote Socket Servers
--------------------------------------
Download the single-module `socketserver.py`_ Python program
and run it like this::
python socketserver.py
It will tell you that it starts listening on the default
port. You can now on your home machine specify this
new socket host with something like this::
pytest -d --tx socket=192.168.1.102:8888 --rsyncdir mypkg
Running tests on many platforms at once
---------------------------------------
The basic command to run tests on multiple platforms is::
pytest --dist=each --tx=spec1 --tx=spec2
If you specify a windows host, an OSX host and a Linux
environment this command will send each tests to all
platforms - and report back failures from all platforms
at once. The specifications strings use the `xspec syntax`_.
.. _`xspec syntax`: https://codespeak.net/execnet/basics.html#xspec
.. _`execnet`: https://codespeak.net/execnet
.. _`socketserver.py`: https://raw.githubusercontent.com/pytest-dev/execnet/master/execnet/script/socketserver.py

16
docs/subprocess.rst Normal file
View File

@@ -0,0 +1,16 @@
Running tests in a Python subprocess
====================================
To instantiate a ``python3.9`` subprocess and send tests to it, you may type::
pytest -d --tx popen//python=python3.9
This will start a subprocess which is run with the ``python3.9``
Python interpreter, found in your system binary lookup path.
If you prefix the --tx option value like this::
--tx 3*popen//python=python3.9
then three subprocesses would be created and tests
will be load-balanced across these three processes.

View File

@@ -1,9 +1,9 @@
.. note:: .. warning::
Since 1.19.0, the actual implementation of the ``--boxed`` option has been moved to a Since 1.19.0, the actual implementation of the ``--boxed`` option has been moved to a
separate plugin, `pytest-forked <https://github.com/pytest-dev/pytest-forked>`_ separate plugin, `pytest-forked <https://github.com/pytest-dev/pytest-forked>`_
which can be installed independently. The ``--boxed`` command-line options remains which can be installed independently. The ``--boxed`` command-line option is deprecated
for backward compatibility reasons. and will be removed in pytest-xdist 3.0.0; use ``--forked`` from pytest-forked instead.
If your testing involves C or C++ libraries you might have to deal If your testing involves C or C++ libraries you might have to deal

View File

@@ -1,3 +1,14 @@
[build-system]
requires = [
"setuptools>=45.0",
"setuptools-scm[toml]>=6.2.3",
"wheel",
]
build-backend = "setuptools.build_meta"
[tool.setuptools_scm]
write_to = "src/xdist/_version.py"
[tool.towncrier] [tool.towncrier]
package = "xdist" package = "xdist"
filename = "CHANGELOG.rst" filename = "CHANGELOG.rst"
@@ -5,32 +16,23 @@ directory = "changelog/"
title_format = "pytest-xdist {version} ({project_date})" title_format = "pytest-xdist {version} ({project_date})"
template = "changelog/_template.rst" template = "changelog/_template.rst"
[[tool.towncrier.type]] [tool.towncrier.fragment.removal]
directory = "removal" name = "Removals"
name = "Deprecations and Removals"
showcontent = true
[[tool.towncrier.type]] [tool.towncrier.fragment.deprecation]
directory = "feature" name = "Deprecations"
[tool.towncrier.fragment.feature]
name = "Features" name = "Features"
showcontent = true
[[tool.towncrier.type]] [tool.towncrier.fragment.bugfix]
directory = "bugfix"
name = "Bug Fixes" name = "Bug Fixes"
showcontent = true
[[tool.towncrier.type]] [tool.towncrier.fragment.vendor]
directory = "vendor"
name = "Vendored Libraries" name = "Vendored Libraries"
showcontent = true
[[tool.towncrier.type]] [tool.towncrier.fragment.doc]
directory = "doc"
name = "Improved Documentation" name = "Improved Documentation"
showcontent = true
[[tool.towncrier.type]] [tool.towncrier.fragment.trivial]
directory = "trivial"
name = "Trivial Changes" name = "Trivial Changes"
showcontent = true

View File

@@ -1,8 +1,81 @@
[bdist_wheel]
universal = 1
[metadata] [metadata]
name = pytest-xdist
description = pytest xdist plugin for distributed testing, most importantly across multiple CPUs
long_description = file: README.rst
license = MIT
author = holger krekel and contributors
author_email = pytest-dev@python.org,holger@merlinux.eu
url = https://github.com/pytest-dev/pytest-xdist
platforms =
linux
osx
win32
classifiers =
Development Status :: 5 - Production/Stable
Framework :: Pytest
Intended Audience :: Developers
License :: OSI Approved :: MIT License
Operating System :: POSIX
Operating System :: Microsoft :: Windows
Operating System :: MacOS :: MacOS X
Topic :: Software Development :: Testing
Topic :: Software Development :: Quality Assurance
Topic :: Utilities
Programming Language :: Python
Programming Language :: Python :: 3
Programming Language :: Python :: 3 :: Only
Programming Language :: Python :: 3.7
Programming Language :: Python :: 3.8
Programming Language :: Python :: 3.9
Programming Language :: Python :: 3.10
Programming Language :: Python :: 3.11
license_file = LICENSE license_file = LICENSE
project_urls =
Documentation=https://pytest-xdist.readthedocs.io/en/latest
Changelog=https://pytest-xdist.readthedocs.io/en/latest/changelog.html
Source=https://github.com/pytest-dev/pytest-xdist
Tracker=https://github.com/pytest-dev/pytest-xdist/issues
[options]
packages = find:
package_dir = =src
zip_safe = False
python_requires = >=3.7
install_requires =
execnet>=1.1
pytest>=6.2.0
[options.packages.find]
where = src
[options.entry_points]
pytest11 =
xdist = xdist.plugin
xdist.looponfail = xdist.looponfail
[options.extras_require]
testing =
filelock
psutil = psutil>=3.0
setproctitle = setproctitle
[flake8] [flake8]
# Ignore any errors related to formatting, let black worry/fix them.
ignore = E501, W503, E203
max-line-length = 100 max-line-length = 100
[mypy]
mypy_path = src
# TODO: Enable this & fix errors.
# check_untyped_defs = True
disallow_any_generics = True
ignore_missing_imports = True
no_implicit_optional = True
show_error_codes = True
strict_equality = True
warn_redundant_casts = True
warn_return_any = True
warn_unreachable = True
warn_unused_configs = True
# TODO: Enable this & fix errors.
# no_implicit_reexport = True

View File

@@ -1,45 +0,0 @@
from setuptools import setup, find_packages
install_requires = ["execnet>=1.1", "pytest>=3.6.0", "pytest-forked", "six"]
setup(
name="pytest-xdist",
use_scm_version={"write_to": "xdist/_version.py"},
description="pytest xdist plugin for distributed testing"
" and loop-on-failing modes",
long_description=open("README.rst").read(),
license="MIT",
author="holger krekel and contributors",
author_email="pytest-dev@python.org,holger@merlinux.eu",
url="https://github.com/pytest-dev/pytest-xdist",
platforms=["linux", "osx", "win32"],
packages=find_packages(exclude=["testing", "example"]),
entry_points={
"pytest11": ["xdist = xdist.plugin", "xdist.looponfail = xdist.looponfail"]
},
zip_safe=False,
python_requires=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*",
install_requires=install_requires,
setup_requires=["setuptools_scm"],
classifiers=[
"Development Status :: 5 - Production/Stable",
"Framework :: Pytest",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Operating System :: POSIX",
"Operating System :: Microsoft :: Windows",
"Operating System :: MacOS :: MacOS X",
"Topic :: Software Development :: Testing",
"Topic :: Software Development :: Quality Assurance",
"Topic :: Utilities",
"Programming Language :: Python",
"Programming Language :: Python :: 2",
"Programming Language :: Python :: 2.7",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.4",
"Programming Language :: Python :: 3.5",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
],
)

15
src/xdist/__init__.py Normal file
View File

@@ -0,0 +1,15 @@
from xdist.plugin import (
is_xdist_worker,
is_xdist_master,
get_xdist_worker_id,
is_xdist_controller,
)
from xdist._version import version as __version__
__all__ = [
"__version__",
"is_xdist_worker",
"is_xdist_master",
"is_xdist_controller",
"get_xdist_worker_id",
]

19
src/xdist/_path.py Normal file
View File

@@ -0,0 +1,19 @@
import os
from itertools import chain
from pathlib import Path
from typing import Callable, Iterator
def visit_path(
path: Path, *, filter: Callable[[Path], bool], recurse: Callable[[Path], bool]
) -> Iterator[Path]:
"""
Implements the interface of ``py.path.local.visit()`` for Path objects,
to simplify porting the code over from ``py.path.local``.
"""
for dirpath, dirnames, filenames in os.walk(path):
dirnames[:] = [x for x in dirnames if recurse(Path(dirpath, x))]
for name in chain(dirnames, filenames):
p = Path(dirpath, name)
if filter(p):
yield p

View File

@@ -1,23 +1,24 @@
import py
import pytest import pytest
from xdist.remote import Producer
from xdist.workermanage import NodeManager from xdist.workermanage import NodeManager
from xdist.scheduler import ( from xdist.scheduler import (
EachScheduling, EachScheduling,
LoadScheduling, LoadScheduling,
LoadScopeScheduling, LoadScopeScheduling,
LoadFileScheduling, LoadFileScheduling,
LoadGroupScheduling,
) )
from six.moves.queue import Empty, Queue from queue import Empty, Queue
class Interrupted(KeyboardInterrupt): class Interrupted(KeyboardInterrupt):
"""signals an immediate interruption.""" """signals an immediate interruption."""
class DSession(object): class DSession:
"""A pytest 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
@@ -33,9 +34,7 @@ class DSession(object):
def __init__(self, config): def __init__(self, config):
self.config = config self.config = config
self.log = py.log.Producer("dsession") self.log = Producer("dsession", enabled=config.option.debug)
if not config.option.debug:
py.log.setconsumer(self.log._keywords, None)
self.nodemanager = None self.nodemanager = None
self.sched = None self.sched = None
self.shuttingdown = False self.shuttingdown = False
@@ -46,14 +45,11 @@ class DSession(object):
self._failed_collection_errors = {} self._failed_collection_errors = {}
self._active_nodes = set() self._active_nodes = set()
self._failed_nodes_count = 0 self._failed_nodes_count = 0
self._max_worker_restart = self.config.option.maxworkerrestart self._max_worker_restart = get_default_max_worker_restart(self.config)
if self._max_worker_restart is not None: # summary message to print at the end of the session
self._max_worker_restart = int(self._max_worker_restart) self._summary_report = None
try:
self.terminal = config.pluginmanager.getplugin("terminalreporter") self.terminal = config.pluginmanager.getplugin("terminalreporter")
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")
@@ -70,7 +66,7 @@ class DSession(object):
if self.terminal and self.config.option.verbose >= 0: if self.terminal and self.config.option.verbose >= 0:
self.terminal.write_line(line) self.terminal.write_line(line)
@pytest.mark.trylast @pytest.hookimpl(trylast=True)
def pytest_sessionstart(self, session): def pytest_sessionstart(self, session):
"""Creates and starts the nodes. """Creates and starts the nodes.
@@ -82,6 +78,7 @@ class DSession(object):
self._active_nodes.update(nodes) self._active_nodes.update(nodes)
self._session = session self._session = session
@pytest.hookimpl
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
@@ -89,11 +86,12 @@ class DSession(object):
nm.teardown_nodes() nm.teardown_nodes()
self._session = None self._session = None
@pytest.hookimpl
def pytest_collection(self): def pytest_collection(self):
# prohibit collection of test items in master process # prohibit collection of test items in controller process
return True return True
@pytest.mark.trylast @pytest.hookimpl(trylast=True)
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 = {
@@ -101,9 +99,11 @@ class DSession(object):
"load": LoadScheduling, "load": LoadScheduling,
"loadscope": LoadScopeScheduling, "loadscope": LoadScopeScheduling,
"loadfile": LoadFileScheduling, "loadfile": LoadFileScheduling,
"loadgroup": LoadGroupScheduling,
} }
return schedulers[dist](config, log) return schedulers[dist](config, log)
@pytest.hookimpl
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, log=self.log config=self.config, log=self.log
@@ -153,9 +153,6 @@ class DSession(object):
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
node.slaveinfo = node.workerinfo
self.config.hook.pytest_testnodeready(node=node) self.config.hook.pytest_testnodeready(node=node)
if self.shuttingdown: if self.shuttingdown:
node.shutdown() node.shutdown()
@@ -172,7 +169,7 @@ class DSession(object):
""" """
self.config.hook.pytest_testnodedown(node=node, error=None) self.config.hook.pytest_testnodedown(node=node, error=None)
if node.workeroutput["exitstatus"] == 2: # keyboard-interrupt if node.workeroutput["exitstatus"] == 2: # keyboard-interrupt
self.shouldstop = "%s received keyboard-interrupt" % (node,) self.shouldstop = f"{node} received keyboard-interrupt"
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:
@@ -180,6 +177,24 @@ class DSession(object):
assert not crashitem, (crashitem, node) assert not crashitem, (crashitem, node)
self._active_nodes.remove(node) self._active_nodes.remove(node)
def worker_internal_error(self, node, formatted_error):
"""
pytest_internalerror() was called on the worker.
pytest_internalerror() arguments are an excinfo and an excrepr, which can't
be serialized, so we go with a poor man's solution of raising an exception
here ourselves using the formatted message.
"""
self._active_nodes.remove(node)
try:
assert False, formatted_error
except AssertionError:
from _pytest._code import ExceptionInfo
excinfo = ExceptionInfo.from_current()
excrepr = excinfo.getrepr()
self.config.hook.pytest_internalerror(excrepr=excrepr, excinfo=excinfo)
def worker_errordown(self, node, error): def worker_errordown(self, node, error):
"""Emitted by the WorkerController when a node dies.""" """Emitted by the WorkerController when a node dies."""
self.config.hook.pytest_testnodedown(node=node, error=error) self.config.hook.pytest_testnodedown(node=node, error=error)
@@ -198,15 +213,25 @@ class DSession(object):
) )
if maximum_reached: if maximum_reached:
if self._max_worker_restart == 0: if self._max_worker_restart == 0:
msg = "Worker restarting disabled" msg = "worker {} crashed and worker restarting disabled".format(
node.gateway.id
)
else: else:
msg = "Maximum crashed workers reached: %d" % self._max_worker_restart msg = "maximum crashed workers reached: %d" % self._max_worker_restart
self.report_line(msg) self._summary_report = msg
self.report_line("\n" + msg)
self.triggershutdown()
else: else:
self.report_line("Replacing crashed worker %s" % node.gateway.id) self.report_line("\nreplacing crashed worker %s" % node.gateway.id)
self.shuttingdown = False
self._clone_node(node) self._clone_node(node)
self._active_nodes.remove(node) self._active_nodes.remove(node)
@pytest.hookimpl
def pytest_terminal_summary(self, terminalreporter):
if self.config.option.verbose >= 0 and self._summary_report:
terminalreporter.write_sep("=", f"xdist: {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.
@@ -220,7 +245,7 @@ class DSession(object):
return return
self.config.hook.pytest_xdist_node_collection_finished(node=node, ids=ids) self.config.hook.pytest_xdist_node_collection_finished(node=node, 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 controller node will finish the session with EXIT_NOTESTSCOLLECTED
self._session.testscollected = len(ids) self._session.testscollected = len(ids)
self.sched.add_node_collection(node, ids) self.sched.add_node_collection(node, ids)
if self.terminal: if self.terminal:
@@ -266,16 +291,20 @@ class DSession(object):
assert not rep.passed assert not rep.passed
self._failed_worker_collectreport(node, rep) self._failed_worker_collectreport(node, rep)
def worker_logwarning(self, message, code, nodeid, fslocation):
"""Emitted when a node calls the pytest_logwarning hook."""
kwargs = dict(message=message, code=code, nodeid=nodeid, fslocation=fslocation)
self.config.hook.pytest_logwarning.call_historic(kwargs=kwargs)
def worker_warning_captured(self, warning_message, when, item): def worker_warning_captured(self, warning_message, when, item):
"""Emitted when a node calls the pytest_logwarning hook.""" """Emitted when a node calls the pytest_warning_captured hook (deprecated in 6.0)."""
# This hook as been removed in pytest 7.1, and we can remove support once we only
# support pytest >=7.1.
kwargs = dict(warning_message=warning_message, when=when, item=item) kwargs = dict(warning_message=warning_message, when=when, item=item)
self.config.hook.pytest_warning_captured.call_historic(kwargs=kwargs) self.config.hook.pytest_warning_captured.call_historic(kwargs=kwargs)
def worker_warning_recorded(self, warning_message, when, nodeid, location):
"""Emitted when a node calls the pytest_warning_recorded hook."""
kwargs = dict(
warning_message=warning_message, when=when, nodeid=nodeid, location=location
)
self.config.hook.pytest_warning_recorded.call_historic(kwargs=kwargs)
def _clone_node(self, node): def _clone_node(self, node):
"""Return new node based on an existing one. """Return new node based on an existing one.
@@ -316,15 +345,21 @@ class DSession(object):
# XXX count no of failures and retry N times # XXX count no of failures and retry N times
runner = self.config.pluginmanager.getplugin("runner") runner = self.config.pluginmanager.getplugin("runner")
fspath = nodeid.split("::")[0] fspath = nodeid.split("::")[0]
msg = "Worker %r crashed while running %r" % (worker.gateway.id, nodeid) msg = f"worker {worker.gateway.id!r} crashed while running {nodeid!r}"
rep = runner.TestReport( rep = runner.TestReport(
nodeid, (fspath, None, fspath), (), "failed", msg, "???" nodeid, (fspath, None, fspath), (), "failed", msg, "???"
) )
rep.node = worker rep.node = worker
self.config.hook.pytest_handlecrashitem(
crashitem=nodeid,
report=rep,
sched=self.sched,
)
self.config.hook.pytest_runtest_logreport(report=rep) self.config.hook.pytest_runtest_logreport(report=rep)
class TerminalDistReporter(object): class TerminalDistReporter:
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")
@@ -346,7 +381,7 @@ class TerminalDistReporter(object):
def getstatus(self): def getstatus(self):
if self.config.option.verbose >= 0: if self.config.option.verbose >= 0:
parts = ["%s %s" % (spec.id, self._status[spec.id]) for spec in self._specs] parts = [f"{spec.id} {self._status[spec.id]}" for spec in self._specs]
return " / ".join(parts) return " / ".join(parts)
else: else:
return "bringing up nodes..." return "bringing up nodes..."
@@ -360,6 +395,7 @@ class TerminalDistReporter(object):
self._lastlen = len(line) self._lastlen = len(line)
self.tr.rewrite(pline, bold=True) self.tr.rewrite(pline, bold=True)
@pytest.hookimpl
def pytest_xdist_setupnodes(self, specs): def pytest_xdist_setupnodes(self, specs):
self._specs = specs self._specs = specs
for spec in specs: for spec in specs:
@@ -367,6 +403,7 @@ class TerminalDistReporter(object):
self.setstatus(spec, "I", show=True) self.setstatus(spec, "I", show=True)
self.ensure_show_status() self.ensure_show_status()
@pytest.hookimpl
def pytest_xdist_newgateway(self, gateway): def pytest_xdist_newgateway(self, gateway):
if self.config.option.verbose > 0: if self.config.option.verbose > 0:
rinfo = gateway._rinfo() rinfo = gateway._rinfo()
@@ -378,22 +415,32 @@ class TerminalDistReporter(object):
) )
self.setstatus(gateway.spec, "C") self.setstatus(gateway.spec, "C")
@pytest.hookimpl
def pytest_testnodeready(self, node): def pytest_testnodeready(self, node):
if self.config.option.verbose > 0: if self.config.option.verbose > 0:
d = node.workerinfo d = node.workerinfo
infoline = "[%s] Python %s" % (d["id"], d["version"].replace("\n", " -- ")) infoline = "[{}] Python {}".format(
d["id"], d["version"].replace("\n", " -- ")
)
self.rewrite(infoline, newline=True) self.rewrite(infoline, newline=True)
self.setstatus(node.gateway.spec, "ok") self.setstatus(node.gateway.spec, "ok")
@pytest.hookimpl
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(f"[{node.gateway.id}] node down: {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

@@ -6,14 +6,20 @@
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 os
import py from pathlib import Path
from typing import Dict, Sequence
import pytest import pytest
import sys import sys
import time import time
import execnet import execnet
from _pytest._io import TerminalWriter
from xdist._path import visit_path
@pytest.hookimpl
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(
@@ -27,19 +33,23 @@ def pytest_addoption(parser):
) )
@pytest.hookimpl
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("--pdb is incompatible with --looponfail.") raise pytest.UsageError("--pdb is 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
def looponfail_main(config): def looponfail_main(config: "pytest.Config") -> None:
remotecontrol = RemoteControl(config) remotecontrol = RemoteControl(config)
rootdirs = config.getini("looponfailroots") config_roots = config.getini("looponfailroots")
if not config_roots:
config_roots = [Path.cwd()]
rootdirs = [Path(root) for root in config_roots]
statrecorder = StatRecorder(rootdirs) statrecorder = StatRecorder(rootdirs)
try: try:
while 1: while 1:
@@ -55,14 +65,14 @@ def looponfail_main(config):
print() print()
class RemoteControl(object): class RemoteControl:
def __init__(self, config): def __init__(self, config):
self.config = config self.config = config
self.failures = [] self.failures = []
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)
print("RemoteControl:", msg) print("RemoteControl:", msg)
def initgateway(self): def initgateway(self):
@@ -70,7 +80,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 = 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")
@@ -128,7 +138,7 @@ class RemoteControl(object):
def repr_pytest_looponfailinfo(failreports, rootdirs): def repr_pytest_looponfailinfo(failreports, rootdirs):
tr = py.io.TerminalWriter() tr = TerminalWriter()
if failreports: if failreports:
tr.sep("#", "LOOPONFAILING", bold=True) tr.sep("#", "LOOPONFAILING", bold=True)
for report in failreports: for report in failreports:
@@ -136,7 +146,7 @@ def repr_pytest_looponfailinfo(failreports, rootdirs):
tr.line(report, red=True) tr.line(report, red=True)
tr.sep("#", "waiting for changes", bold=True) tr.sep("#", "waiting for changes", bold=True)
for rootdir in rootdirs: for rootdir in rootdirs:
tr.line("### Watching: %s" % (rootdir,), bold=True) tr.line(f"### Watching: {rootdir}", bold=True)
def init_worker_session(channel, args, option_dict): def init_worker_session(channel, args, option_dict):
@@ -165,7 +175,7 @@ def init_worker_session(channel, args, option_dict):
WorkerFailSession(config, channel).main() WorkerFailSession(config, channel).main()
class WorkerFailSession(object): class WorkerFailSession:
def __init__(self, config, channel): def __init__(self, config, channel):
self.config = config self.config = config
self.channel = channel self.channel = channel
@@ -179,6 +189,7 @@ class WorkerFailSession(object):
if self.config.option.debug: if self.config.option.debug:
print(" ".join(map(str, args))) print(" ".join(map(str, args)))
@pytest.hookimpl
def pytest_collection(self, session): def pytest_collection(self, session):
self.session = session self.session = session
self.trails = self.current_command self.trails = self.current_command
@@ -193,10 +204,12 @@ class WorkerFailSession(object):
hook.pytest_collection_finish(session=session) hook.pytest_collection_finish(session=session)
return True return True
@pytest.hookimpl
def pytest_runtest_logreport(self, report): def pytest_runtest_logreport(self, report):
if report.failed: if report.failed:
self.recorded_failures.append(report) self.recorded_failures.append(report)
@pytest.hookimpl
def pytest_collectreport(self, report): def pytest_collectreport(self, report):
if report.failed: if report.failed:
self.recorded_failures.append(report) self.recorded_failures.append(report)
@@ -220,17 +233,17 @@ class WorkerFailSession(object):
self.channel.send((trails, failreports, self.collection_failed)) self.channel.send((trails, failreports, self.collection_failed))
class StatRecorder(object): class StatRecorder:
def __init__(self, rootdirlist): def __init__(self, rootdirlist: Sequence[Path]) -> None:
self.rootdirlist = rootdirlist self.rootdirlist = rootdirlist
self.statcache = {} self.statcache: Dict[Path, os.stat_result] = {}
self.check() # snapshot state self.check() # snapshot state
def fil(self, p): def fil(self, p: Path) -> bool:
return p.check(file=1, dotfile=0) and p.ext != ".pyc" return p.is_file() and not p.name.startswith(".") and p.suffix != ".pyc"
def rec(self, p): def rec(self, p: Path) -> bool:
return p.check(dotfile=0) return not p.name.startswith(".") and p.exists()
def waitonchange(self, checkinterval=1.0): def waitonchange(self, checkinterval=1.0):
while 1: while 1:
@@ -239,34 +252,34 @@ class StatRecorder(object):
return return
time.sleep(checkinterval) time.sleep(checkinterval)
def check(self, removepycfiles=True): # noqa, too complex def check(self, removepycfiles: bool = True) -> bool: # noqa, too complex
changed = False changed = False
statcache = self.statcache newstat: Dict[Path, os.stat_result] = {}
newstat = {}
for rootdir in self.rootdirlist: for rootdir in self.rootdirlist:
for path in rootdir.visit(self.fil, self.rec): for path in visit_path(rootdir, filter=self.fil, recurse=self.rec):
oldstat = statcache.pop(path, None) oldstat = self.statcache.pop(path, None)
try: try:
newstat[path] = curstat = path.stat() curstat = path.stat()
except py.error.ENOENT: except OSError:
if oldstat: if oldstat:
changed = True changed = True
else: else:
if oldstat: newstat[path] = curstat
if oldstat is not None:
if ( if (
oldstat.mtime != curstat.mtime oldstat.st_mtime != curstat.st_mtime
or oldstat.size != curstat.size or oldstat.st_size != curstat.st_size
): ):
changed = True changed = True
print("# MODIFIED", path) print("# MODIFIED", path)
if removepycfiles and path.ext == ".py": if removepycfiles and path.suffix == ".py":
pycfile = path + "c" pycfile = path.with_suffix(".pyc")
if pycfile.check(): if pycfile.is_file():
pycfile.remove() os.unlink(pycfile)
else: else:
changed = True changed = True
if statcache: if self.statcache:
changed = True changed = True
self.statcache = newstat self.statcache = newstat
return changed return changed

95
src/xdist/newhooks.py Normal file
View File

@@ -0,0 +1,95 @@
"""
xdist hooks.
Additionally, pytest-xdist will also decorate a few other hooks
with the worker instance that executed the hook originally:
``pytest_runtest_logreport``: ``rep`` parameter has a ``node`` attribute.
You can use this hooks just as you would use normal pytest hooks, but some care
must be taken in plugins in case ``xdist`` is not installed. Please see:
http://pytest.org/en/latest/writing_plugins.html#optionally-using-hooks-from-3rd-party-plugins
"""
import pytest
@pytest.hookspec()
def pytest_xdist_setupnodes(config, specs):
"""called before any remote node is set up."""
@pytest.hookspec()
def pytest_xdist_newgateway(gateway):
"""called on new raw gateway creation."""
@pytest.hookspec(
warn_on_impl="rsync feature is deprecated and will be removed in pytest-xdist 4.0"
)
def pytest_xdist_rsyncstart(source, gateways):
"""called before rsyncing a directory to remote gateways takes place."""
@pytest.hookspec(
warn_on_impl="rsync feature is deprecated and will be removed in pytest-xdist 4.0"
)
def pytest_xdist_rsyncfinish(source, gateways):
"""called after rsyncing a directory to remote gateways takes place."""
@pytest.hookspec(firstresult=True)
def pytest_xdist_getremotemodule():
"""called when creating remote node"""
@pytest.hookspec()
def pytest_configure_node(node):
"""configure node information before it gets instantiated."""
@pytest.hookspec()
def pytest_testnodeready(node):
"""Test Node is ready to operate."""
@pytest.hookspec()
def pytest_testnodedown(node, error):
"""Test Node is down."""
@pytest.hookspec()
def pytest_xdist_node_collection_finished(node, ids):
"""called by the controller node when a worker node finishes collecting."""
@pytest.hookspec(firstresult=True)
def pytest_xdist_make_scheduler(config, log):
"""return a node scheduler implementation"""
@pytest.hookspec(firstresult=True)
def pytest_xdist_auto_num_workers(config):
"""
Return the number of workers to spawn when ``--numprocesses=auto`` is given in the
command-line.
.. versionadded:: 2.1
"""
@pytest.hookspec(firstresult=True)
def pytest_handlecrashitem(crashitem, report, sched):
"""
Handle a crashitem, modifying the report if necessary.
The scheduler is provided as a parameter to reschedule the test if desired with
`sched.mark_test_pending`.
def pytest_handlecrashitem(crashitem, report, sched):
if should_rerun(crashitem):
sched.mark_test_pending(crashitem)
report.outcome = "rerun"
.. versionadded:: 2.2.1
"""

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

@@ -0,0 +1,319 @@
import os
import uuid
import sys
import warnings
import pytest
PYTEST_GTE_7 = hasattr(pytest, "version_tuple") and pytest.version_tuple >= (7, 0) # type: ignore[attr-defined]
_sys_path = list(sys.path) # freeze a copy of sys.path at interpreter startup
@pytest.hookimpl
def pytest_xdist_auto_num_workers(config):
env_var = os.environ.get("PYTEST_XDIST_AUTO_NUM_WORKERS")
if env_var:
try:
return int(env_var)
except ValueError:
warnings.warn(
"PYTEST_XDIST_AUTO_NUM_WORKERS is not a number: {env_var!r}. Ignoring it."
)
try:
import psutil
except ImportError:
pass
else:
use_logical = config.option.numprocesses == "logical"
count = psutil.cpu_count(logical=use_logical) or psutil.cpu_count()
if count:
return count
try:
from os import sched_getaffinity
def cpu_count():
return len(sched_getaffinity(0))
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
try:
n = cpu_count()
except NotImplementedError:
return 1
return n if n else 1
def parse_numprocesses(s):
if s in ("auto", "logical"):
return s
elif s is not None:
return int(s)
@pytest.hookimpl
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'. With 'auto', attempt "
"to detect physical CPU count. With 'logical', detect logical CPU "
"count. If physical CPU count cannot be found, falls back to logical "
"count. This 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",
action="store",
default=None,
dest="maxworkerrestart",
help="maximum number of workers that can be restarted "
"when crashed (set to zero to disable this feature)",
)
group.addoption(
"--dist",
metavar="distmode",
action="store",
choices=["each", "load", "loadscope", "loadfile", "loadgroup", "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"
"loadgroup: like load, but sends tests marked with 'xdist_group' to the same worker.\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(
"--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="paths" if PYTEST_GTE_7 else "pathlist",
)
parser.addini(
"rsyncignore",
"list of (relative) glob-style paths to be ignored for rsyncing.",
type="paths" if PYTEST_GTE_7 else "pathlist",
)
parser.addini(
"looponfailroots",
type="paths" if PYTEST_GTE_7 else "pathlist",
help="directories to check for changes. Default: current directory.",
)
# -------------------------------------------------------------------------
# distributed testing hooks
# -------------------------------------------------------------------------
@pytest.hookimpl
def pytest_addhooks(pluginmanager):
from xdist import newhooks
pluginmanager.add_hookspecs(newhooks)
# -------------------------------------------------------------------------
# distributed testing initialization
# -------------------------------------------------------------------------
@pytest.hookimpl(trylast=True)
def pytest_configure(config):
config_line = (
"xdist_group: specify group for tests should run in same session."
"in relation to one another. Provided by pytest-xdist."
)
config.addinivalue_line("markers", config_line)
# Skip this plugin entirely when only doing collection.
if config.getvalue("collectonly"):
return
# Create the distributed session in case we have a valid distribution
# mode and test environments.
if config.getoption("dist") != "no" and config.getoption("tx"):
from xdist.dsession import DSession
session = DSession(config)
config.pluginmanager.register(session, "dsession")
tr = config.pluginmanager.getplugin("terminalreporter")
if tr:
tr.showfspath = False
# Deprecation warnings for deprecated command-line/configuration options.
if config.getoption("looponfail", None) or config.getini("looponfailroots"):
warning = DeprecationWarning(
"The --looponfail command line argument and looponfailroots config variable are deprecated.\n"
"The loop-on-fail feature will be removed in pytest-xdist 4.0."
)
config.issue_config_time_warning(warning, 2)
if config.getoption("rsyncdir", None) or config.getini("rsyncdirs"):
warning = DeprecationWarning(
"The --rsyncdir command line argument and rsyncdirs config variable are deprecated.\n"
"The rsync feature will be removed in pytest-xdist 4.0."
)
config.issue_config_time_warning(warning, 2)
@pytest.hookimpl(tryfirst=True)
def pytest_cmdline_main(config):
usepdb = config.getoption("usepdb", False) # a core option
if config.option.numprocesses in ("auto", "logical"):
if usepdb:
config.option.numprocesses = 0
config.option.dist = "no"
else:
auto_num_cpus = config.hook.pytest_xdist_auto_num_workers(config=config)
config.option.numprocesses = auto_num_cpus
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") and val("dist") != "no" and usepdb:
raise pytest.UsageError(
"--pdb is incompatible with distributing tests; try using -n0 or -nauto."
) # noqa: E501
# -------------------------------------------------------------------------
# fixtures and API to easily know the role of current node
# -------------------------------------------------------------------------
def is_xdist_worker(request_or_session) -> bool:
"""Return `True` if this is an xdist worker, `False` otherwise
:param request_or_session: the `pytest` `request` or `session` object
"""
return hasattr(request_or_session.config, "workerinput")
def is_xdist_controller(request_or_session) -> bool:
"""Return `True` if this is the xdist controller, `False` otherwise
Note: this method also returns `False` when distribution has not been
activated at all.
:param request_or_session: the `pytest` `request` or `session` object
"""
return (
not is_xdist_worker(request_or_session)
and request_or_session.config.option.dist != "no"
)
# ALIAS: TODO, deprecate (#592)
is_xdist_master = is_xdist_controller
def get_xdist_worker_id(request_or_session):
"""Return the id of the current worker ('gw0', 'gw1', etc) or 'master'
if running on the controller node.
If not distributing tests (for example passing `-n0` or not passing `-n` at all)
also return 'master'.
:param request_or_session: the `pytest` `request` or `session` object
"""
if hasattr(request_or_session.config, "workerinput"):
return request_or_session.config.workerinput["workerid"]
else:
# TODO: remove "master", ideally for a None
return "master"
@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.
"""
# TODO: remove "master", ideally for a None
return get_xdist_worker_id(request)
@pytest.fixture(scope="session")
def testrun_uid(request):
"""Return the unique id of the current test."""
if hasattr(request.config, "workerinput"):
return request.config.workerinput["testrunuid"]
else:
return uuid.uuid4().hex

View File

@@ -9,19 +9,58 @@
import sys import sys
import os import os
import time import time
from typing import Any
import _pytest.hookspec
import pytest import pytest
from execnet.gateway_base import dumps, DumpError from execnet.gateway_base import dumps, DumpError
from _pytest.config import _prepareconfig, Config
class WorkerInteractor(object): try:
from setproctitle import setproctitle
except ImportError:
def setproctitle(title):
pass
class Producer:
"""
Simplified implementation of the same interface as py.log, for backward compatibility
since we dropped the dependency on pylib.
Note: this is defined here because this module can't depend on xdist, so we need
to have the other way around.
"""
def __init__(self, name: str, *, enabled: bool = True):
self.name = name
self.enabled = enabled
def __repr__(self) -> str:
return f"{type(self).__name__}({self.name!r}, enabled={self.enabled})"
def __call__(self, *a: Any, **k: Any) -> None:
if self.enabled:
print(f"[{self.name}]", *a, **k, file=sys.stderr)
def __getattr__(self, name: str) -> "Producer":
return type(self)(name, enabled=self.enabled)
def worker_title(title):
try:
setproctitle(title)
except Exception:
# changing the process name is very optional, no errors please
pass
class WorkerInteractor:
def __init__(self, config, channel): def __init__(self, config, channel):
self.config = config self.config = config
self.workerid = config.workerinput.get("workerid", "?") self.workerid = config.workerinput.get("workerid", "?")
self.log = py.log.Producer("worker-%s" % self.workerid) self.testrunuid = config.workerinput["testrunuid"]
if not config.option.debug: self.log = Producer(f"worker-{self.workerid}", enabled=config.option.debug)
py.log.setconsumer(self.log._keywords, None)
self.channel = channel self.channel = channel
config.pluginmanager.register(self) config.pluginmanager.register(self)
@@ -29,10 +68,14 @@ class WorkerInteractor(object):
self.log("sending", name, kwargs) self.log("sending", name, kwargs)
self.channel.send((name, kwargs)) self.channel.send((name, kwargs))
@pytest.hookimpl
def pytest_internalerror(self, excrepr): def pytest_internalerror(self, excrepr):
for line in str(excrepr).split("\n"): formatted_error = str(excrepr)
for line in formatted_error.split("\n"):
self.log("IERROR>", line) self.log("IERROR>", line)
interactor.sendevent("internal_error", formatted_error=formatted_error)
@pytest.hookimpl
def pytest_sessionstart(self, session): def pytest_sessionstart(self, session):
self.session = session self.session = session
workerinfo = getinfodict() workerinfo = getinfodict()
@@ -40,13 +83,16 @@ class WorkerInteractor(object):
@pytest.hookimpl(hookwrapper=True) @pytest.hookimpl(hookwrapper=True)
def pytest_sessionfinish(self, exitstatus): def pytest_sessionfinish(self, exitstatus):
self.config.workeroutput["exitstatus"] = exitstatus # in pytest 5.0+, exitstatus is an IntEnum object
self.config.workeroutput["exitstatus"] = int(exitstatus)
yield yield
self.sendevent("workerfinished", workeroutput=self.config.workeroutput) self.sendevent("workerfinished", workeroutput=self.config.workeroutput)
@pytest.hookimpl
def pytest_collection(self, session): def pytest_collection(self, session):
self.sendevent("collectionstart") self.sendevent("collectionstart")
@pytest.hookimpl
def pytest_runtestloop(self, session): def pytest_runtestloop(self, session):
self.log("entering main loop") self.log("entering main loop")
torun = [] torun = []
@@ -79,115 +125,91 @@ class WorkerInteractor(object):
else: else:
nextitem = None nextitem = None
worker_title("[pytest-xdist running] %s" % item.nodeid)
start = time.time() start = time.time()
self.config.hook.pytest_runtest_protocol(item=item, nextitem=nextitem) self.config.hook.pytest_runtest_protocol(item=item, nextitem=nextitem)
duration = time.time() - start duration = time.time() - start
worker_title("[pytest-xdist idle]")
self.sendevent( self.sendevent(
"runtest_protocol_complete", item_index=self.item_index, duration=duration "runtest_protocol_complete", item_index=self.item_index, duration=duration
) )
def pytest_collection_modifyitems(self, session, config, items):
# add the group name to nodeid as suffix if --dist=loadgroup
if config.getvalue("loadgroup"):
for item in items:
mark = item.get_closest_marker("xdist_group")
if not mark:
continue
gname = (
mark.args[0]
if len(mark.args) > 0
else mark.kwargs.get("name", "default")
)
item._nodeid = f"{item.nodeid}@{gname}"
@pytest.hookimpl
def pytest_collection_finish(self, session): def pytest_collection_finish(self, session):
try:
topdir = str(self.config.rootpath)
except AttributeError: # pytest <= 6.1.0
topdir = str(self.config.rootdir)
self.sendevent( self.sendevent(
"collectionfinish", "collectionfinish",
topdir=str(session.fspath), topdir=topdir,
ids=[item.nodeid for item in session.items], ids=[item.nodeid for item in session.items],
) )
@pytest.hookimpl
def pytest_runtest_logstart(self, nodeid, location): def pytest_runtest_logstart(self, nodeid, location):
self.sendevent("logstart", nodeid=nodeid, location=location) self.sendevent("logstart", nodeid=nodeid, location=location)
# the pytest_runtest_logfinish hook was introduced in pytest 3.4 @pytest.hookimpl
if hasattr(_pytest.hookspec, "pytest_runtest_logfinish"):
def pytest_runtest_logfinish(self, nodeid, location): def pytest_runtest_logfinish(self, nodeid, location):
self.sendevent("logfinish", nodeid=nodeid, location=location) self.sendevent("logfinish", nodeid=nodeid, location=location)
@pytest.hookimpl
def pytest_runtest_logreport(self, report): def pytest_runtest_logreport(self, report):
data = serialize_report(report) data = self.config.hook.pytest_report_to_serializable(
config=self.config, report=report
)
data["item_index"] = self.item_index data["item_index"] = self.item_index
data["worker_id"] = self.workerid data["worker_id"] = self.workerid
data["testrun_uid"] = self.testrunuid
assert self.session.items[self.item_index].nodeid == report.nodeid assert self.session.items[self.item_index].nodeid == report.nodeid
self.sendevent("testreport", data=data) self.sendevent("testreport", data=data)
@pytest.hookimpl
def pytest_collectreport(self, report): def pytest_collectreport(self, report):
# send only reports that have not passed to master as optimization (#330) # send only reports that have not passed to controller as optimization (#330)
if not report.passed: if not report.passed:
data = serialize_report(report) data = self.config.hook.pytest_report_to_serializable(
config=self.config, report=report
)
self.sendevent("collectreport", data=data) self.sendevent("collectreport", data=data)
# the pytest_logwarning hook was removed in pytest 4.1 @pytest.hookimpl
if hasattr(_pytest.hookspec, "pytest_logwarning"): def pytest_warning_recorded(self, warning_message, when, nodeid, location):
def pytest_logwarning(self, message, code, nodeid, fslocation):
self.sendevent( self.sendevent(
"logwarning", "warning_recorded",
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), warning_message_data=serialize_warning_message(warning_message),
when=when, when=when,
# item cannot be serialized and will always be None when used with xdist nodeid=nodeid,
item=None, location=location,
) )
def serialize_report(rep):
def disassembled_report(rep):
reprtraceback = rep.longrepr.reprtraceback.__dict__.copy()
reprcrash = rep.longrepr.reprcrash.__dict__.copy()
new_entries = []
for entry in reprtraceback["reprentries"]:
entry_data = {"type": type(entry).__name__, "data": entry.__dict__.copy()}
for key, value in entry_data["data"].items():
if hasattr(value, "__dict__"):
entry_data["data"][key] = value.__dict__.copy()
new_entries.append(entry_data)
reprtraceback["reprentries"] = new_entries
return {
"reprcrash": reprcrash,
"reprtraceback": reprtraceback,
"sections": rep.longrepr.sections,
}
import py
d = rep.__dict__.copy()
if hasattr(rep.longrepr, "toterminal"):
if hasattr(rep.longrepr, "reprtraceback") and hasattr(
rep.longrepr, "reprcrash"
):
d["longrepr"] = disassembled_report(rep)
else:
d["longrepr"] = str(rep.longrepr)
else:
d["longrepr"] = rep.longrepr
for name in d:
if isinstance(d[name], py.path.local):
d[name] = str(d[name])
elif name == "result":
d[name] = None # for now
return d
def serialize_warning_message(warning_message): def serialize_warning_message(warning_message):
if isinstance(warning_message.message, Warning): if isinstance(warning_message.message, Warning):
message_module = type(warning_message.message).__module__ message_module = type(warning_message.message).__module__
message_class_name = type(warning_message.message).__name__ message_class_name = type(warning_message.message).__name__
message_str = str(warning_message.message) message_str = str(warning_message.message)
# check now if we can serialize the warning arguments (#349) # check now if we can serialize the warning arguments (#349)
# if not, we will just use the exception message on the master node # if not, we will just use the exception message on the controller node
try: try:
dumps(warning_message.message.args) dumps(warning_message.message.args)
except DumpError: except DumpError:
@@ -218,7 +240,15 @@ def serialize_warning_message(warning_message):
for attr_name in warning_message._WARNING_DETAILS: for attr_name in warning_message._WARNING_DETAILS:
if attr_name in ("message", "category"): if attr_name in ("message", "category"):
continue continue
result[attr_name] = getattr(warning_message, attr_name) attr = getattr(warning_message, attr_name)
# Check if we can serialize the warning detail, marking `None` otherwise
# Note that we need to define the attr (even as `None`) to allow deserializing
try:
dumps(attr)
except DumpError:
result[attr_name] = repr(attr)
else:
result[attr_name] = attr
return result return result
@@ -236,39 +266,47 @@ def getinfodict():
def remote_initconfig(option_dict, args): def remote_initconfig(option_dict, args):
from _pytest.config import Config
option_dict["plugins"].append("no:terminal") option_dict["plugins"].append("no:terminal")
config = Config.fromdictargs(option_dict, args) return Config.fromdictargs(option_dict, args)
def setup_config(config, basetemp):
config.option.loadgroup = config.getvalue("dist") == "loadgroup"
config.option.looponfail = False config.option.looponfail = False
config.option.usepdb = False config.option.usepdb = False
config.option.dist = "no" config.option.dist = "no"
config.option.distload = False config.option.distload = False
config.option.numprocesses = None config.option.numprocesses = None
config.option.maxprocesses = None config.option.maxprocesses = None
config.args = args config.option.basetemp = basetemp
return config
if __name__ == "__channelexec__": if __name__ == "__channelexec__":
channel = channel # noqa channel = channel # type: ignore[name-defined] # noqa: F821
workerinput, args, option_dict = channel.receive() workerinput, args, option_dict, change_sys_path = channel.receive() # type: ignore[name-defined]
if change_sys_path is None:
importpath = os.getcwd() importpath = os.getcwd()
sys.path.insert(0, importpath) # XXX only for remote situations sys.path.insert(0, importpath)
os.environ["PYTHONPATH"] = ( os.environ["PYTHONPATH"] = (
importpath + os.pathsep + os.environ.get("PYTHONPATH", "") importpath + os.pathsep + os.environ.get("PYTHONPATH", "")
) )
else:
sys.path = change_sys_path
os.environ["PYTEST_XDIST_TESTRUNUID"] = workerinput["testrunuid"]
os.environ["PYTEST_XDIST_WORKER"] = workerinput["workerid"] os.environ["PYTEST_XDIST_WORKER"] = workerinput["workerid"]
os.environ["PYTEST_XDIST_WORKER_COUNT"] = str(workerinput["workercount"]) os.environ["PYTEST_XDIST_WORKER_COUNT"] = str(workerinput["workercount"])
# os.environ['PYTHONPATH'] = importpath
import py
if hasattr(Config, "InvocationParams"):
config = _prepareconfig(args, None)
else:
config = remote_initconfig(option_dict, args) config = remote_initconfig(option_dict, args)
config.args = args
setup_config(config, option_dict.get("basetemp"))
config._parser.prog = os.path.basename(workerinput["mainargv"][0]) config._parser.prog = os.path.basename(workerinput["mainargv"][0])
config.workerinput = workerinput config.workerinput = workerinput # type: ignore[attr-defined]
config.workeroutput = {} config.workeroutput = {} # type: ignore[attr-defined]
# TODO: deprecated name, backward compatibility only. Remove it in future interactor = WorkerInteractor(config, channel) # type: ignore[name-defined]
config.slaveinput = config.workerinput
config.slaveoutput = config.workeroutput
interactor = WorkerInteractor(config, channel)
config.hook.pytest_cmdline_main(config=config) config.hook.pytest_cmdline_main(config=config)

View File

@@ -1,4 +1,3 @@
from __future__ import print_function
from difflib import unified_diff from difflib import unified_diff
@@ -13,9 +12,10 @@ def report_collection_diff(from_collection, to_collection, from_id, to_id):
diff = unified_diff(from_collection, to_collection, fromfile=from_id, tofile=to_id) diff = unified_diff(from_collection, to_collection, fromfile=from_id, tofile=to_id)
error_message = ( error_message = (
u"Different tests were collected between {from_id} and {to_id}. " "Different tests were collected between {from_id} and {to_id}. "
u"The difference is:\n" "The difference is:\n"
u"{diff}" "{diff}\n"
"To see why this happens see Known limitations in documentation"
).format(from_id=from_id, to_id=to_id, diff="\n".join(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

@@ -2,3 +2,4 @@ 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.loadfile import LoadFileScheduling # noqa
from xdist.scheduler.loadscope import LoadScopeScheduling # noqa from xdist.scheduler.loadscope import LoadScopeScheduling # noqa
from xdist.scheduler.loadgroup import LoadGroupScheduling # noqa

View File

@@ -1,10 +1,9 @@
from py.log import Producer from xdist.remote import Producer
from xdist.workermanage import parse_spec_config from xdist.workermanage import parse_spec_config
from xdist.report import report_collection_diff from xdist.report import report_collection_diff
class EachScheduling(object): class EachScheduling:
"""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
@@ -101,6 +100,14 @@ class EachScheduling(object):
def mark_test_complete(self, node, item_index, duration=0): def mark_test_complete(self, node, item_index, duration=0):
self.node2pending[node].remove(item_index) self.node2pending[node].remove(item_index)
def mark_test_pending(self, item):
self.pending.insert(
0,
self.collection.index(item),
)
for node in self.node2pending:
self.check_schedule(node)
def remove_node(self, node): def remove_node(self, node):
# KeyError if we didn't get an add_node() yet # KeyError if we didn't get an add_node() yet
pending = self.node2pending.pop(node) pending = self.node2pending.pop(node)

View File

@@ -1,13 +1,13 @@
from itertools import cycle from itertools import cycle
from py.log import Producer
from _pytest.runner import CollectReport from _pytest.runner import CollectReport
from xdist.remote import Producer
from xdist.workermanage import parse_spec_config from xdist.workermanage import parse_spec_config
from xdist.report import report_collection_diff from xdist.report import report_collection_diff
class LoadScheduling(object): class LoadScheduling:
"""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
@@ -151,6 +151,14 @@ class LoadScheduling(object):
self.node2pending[node].remove(item_index) self.node2pending[node].remove(item_index)
self.check_schedule(node, duration=duration) self.check_schedule(node, duration=duration)
def mark_test_pending(self, item):
self.pending.insert(
0,
self.collection.index(item),
)
for node in self.node2pending:
self.check_schedule(node)
def check_schedule(self, node, duration=0): def check_schedule(self, node, duration=0):
"""Maybe schedule new items on the node """Maybe schedule new items on the node
@@ -240,13 +248,27 @@ class LoadScheduling(object):
# 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, 2 * len(self.nodes)) if len(self.pending) < 2 * len(self.nodes):
# Distribute tests round-robin. Try to load all nodes if there are
# distribute tests round-robin up to the batch size # enough tests. The other branch tries sends at least 2 tests
# (or until we run out) # to each node - which is suboptimal when you have less than
# 2 * len(nodes) tests.
nodes = cycle(self.nodes) nodes = cycle(self.nodes)
for i in range(initial_batch): for i in range(len(self.pending)):
self._send_tests(next(nodes), 1) self._send_tests(next(nodes), 1)
else:
# Send batches of consecutive tests. By default, pytest sorts tests
# in order for optimal single-threaded execution, minimizing the
# number of necessary fixture setup/teardown. Try to keep that
# optimal order for every worker.
# how many items per node do we have about?
items_per_node = len(self.collection) // len(self.node2pending)
# take a fraction of tests for initial distribution
node_chunksize = max(items_per_node // 4, 2)
# and initialize each node with a chunk of tests
for node in self.nodes:
self._send_tests(node, node_chunksize)
if not self.pending: if not self.pending:
# initial distribution sent all tests, start node shutdown # initial distribution sent all tests, start node shutdown

View File

@@ -1,5 +1,5 @@
from .loadscope import LoadScopeScheduling from .loadscope import LoadScopeScheduling
from py.log import Producer from xdist.remote import Producer
class LoadFileScheduling(LoadScopeScheduling): class LoadFileScheduling(LoadScopeScheduling):
@@ -21,7 +21,7 @@ class LoadFileScheduling(LoadScopeScheduling):
""" """
def __init__(self, config, log=None): def __init__(self, config, log=None):
super(LoadFileScheduling, self).__init__(config, log) super().__init__(config, log)
if log is None: if log is None:
self.log = Producer("loadfilesched") self.log = Producer("loadfilesched")
else: else:

View File

@@ -0,0 +1,54 @@
from .loadscope import LoadScopeScheduling
from xdist.remote import Producer
class LoadGroupScheduling(LoadScopeScheduling):
"""Implement load scheduling across nodes, but grouping test by xdist_group mark.
This class behaves very much like LoadScopeScheduling, but it groups tests by xdist_group mark
instead of the module or class to which they belong to.
"""
def __init__(self, config, log=None):
super().__init__(config, log)
if log is None:
self.log = Producer("loadgroupsched")
else:
self.log = log.loadgroupsched
def _split_scope(self, nodeid):
"""Determine the scope (grouping) of a nodeid.
There are usually 3 cases for a nodeid::
example/loadsuite/test/test_beta.py::test_beta0
example/loadsuite/test/test_delta.py::Delta1::test_delta0
example/loadsuite/epsilon/__init__.py::epsilon.epsilon
#. Function in a test module.
#. Method of a class in a test module.
#. Doctest in a function in a package.
With loadgroup, two cases are added::
example/loadsuite/test/test_beta.py::test_beta0
example/loadsuite/test/test_delta.py::Delta1::test_delta0
example/loadsuite/epsilon/__init__.py::epsilon.epsilon
example/loadsuite/test/test_gamma.py::test_beta0@gname
example/loadsuite/test/test_delta.py::Gamma1::test_gamma0@gname
This function will group tests with the scope determined by splitting the first ``@``
from the right. That is, test will be grouped in a single work unit when they have
same group name. In the above example, scopes will be::
example/loadsuite/test/test_beta.py::test_beta0
example/loadsuite/test/test_delta.py::Delta1::test_delta0
example/loadsuite/epsilon/__init__.py::epsilon.epsilon
gname
gname
"""
if nodeid.rfind("@") > nodeid.rfind("]"):
# check the index of ']' to avoid the case: parametrize mark value has '@'
return nodeid.split("@")[-1]
else:
return nodeid

View File

@@ -1,12 +1,12 @@
from collections import OrderedDict from collections import OrderedDict
from _pytest.runner import CollectReport from _pytest.runner import CollectReport
from py.log import Producer from xdist.remote import Producer
from xdist.report import report_collection_diff from xdist.report import report_collection_diff
from xdist.workermanage import parse_spec_config from xdist.workermanage import parse_spec_config
class LoadScopeScheduling(object): class LoadScopeScheduling:
"""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
@@ -243,6 +243,9 @@ class LoadScopeScheduling(object):
self.assigned_work[node][scope][nodeid] = True self.assigned_work[node][scope][nodeid] = True
self._reschedule(node) self._reschedule(node)
def mark_test_pending(self, item):
raise NotImplementedError()
def _assign_work_unit(self, node): def _assign_work_unit(self, node):
"""Assign a work unit to a node.""" """Assign a work unit to a node."""
assert self.workqueue assert self.workqueue
@@ -358,12 +361,12 @@ class LoadScopeScheduling(object):
extra_nodes = len(self.nodes) - len(self.workqueue) extra_nodes = len(self.nodes) - len(self.workqueue)
if extra_nodes > 0: if extra_nodes > 0:
self.log("Shuting down {0} nodes".format(extra_nodes)) self.log(f"Shutting down {extra_nodes} 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(f"Shutting down unused node {unused_node}")
unused_node.shutdown() unused_node.shutdown()
# Assign initial workload # Assign initial workload

View File

@@ -1,17 +1,17 @@
from __future__ import print_function
import fnmatch import fnmatch
import os import os
import re import re
import sys import sys
import threading import uuid
from pathlib import Path
from typing import List, Union, Sequence, Optional, Any, Tuple, Set
import py
import pytest import pytest
import execnet import execnet
import xdist.remote import xdist.remote
from xdist.remote import Producer
from _pytest import runner # XXX load dynamically from xdist.plugin import _sys_path
def parse_spec_config(config): def parse_spec_config(config):
@@ -31,14 +31,16 @@ def parse_spec_config(config):
return xspeclist return xspeclist
class NodeManager(object): class NodeManager:
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") -> None:
self.config = config self.config = config
self._nodesready = 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()
@@ -52,7 +54,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 = set() self._rsynced_specs: Set[Tuple[Any, Any]] = 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."""
@@ -63,10 +65,7 @@ class NodeManager(object):
def setup_nodes(self, putevent): def setup_nodes(self, putevent):
self.config.hook.pytest_xdist_setupnodes(config=self.config, specs=self.specs) self.config.hook.pytest_xdist_setupnodes(config=self.config, specs=self.specs)
self.trace("setting up nodes") self.trace("setting up nodes")
nodes = [] return [self.setup_node(spec, putevent) for spec in self.specs]
for spec in self.specs:
nodes.append(self.setup_node(spec, putevent))
return nodes
def setup_node(self, spec, putevent): def setup_node(self, spec, putevent):
gw = self.group.makegateway(spec) gw = self.group.makegateway(spec)
@@ -84,7 +83,7 @@ class NodeManager(object):
def _getxspecs(self): def _getxspecs(self):
return [execnet.XSpec(x) for x in parse_spec_config(self.config)] return [execnet.XSpec(x) for x in parse_spec_config(self.config)]
def _getrsyncdirs(self): def _getrsyncdirs(self) -> List[Path]:
for spec in self.specs: for spec in self.specs:
if not spec.popen or spec.chdir: if not spec.popen or spec.chdir:
break break
@@ -93,19 +92,27 @@ class NodeManager(object):
import pytest import pytest
import _pytest import _pytest
pytestpath = pytest.__file__.rstrip("co") def get_dir(p):
pytestdir = py.path.local(_pytest.__file__).dirpath() """Return the directory path if p is a package or the path to the .py file otherwise."""
stripped = p.rstrip("co")
if os.path.basename(stripped) == "__init__.py":
return os.path.dirname(p)
else:
return stripped
pytestpath = get_dir(pytest.__file__)
pytestdir = get_dir(_pytest.__file__)
config = self.config config = self.config
candidates = [py._pydir, pytestpath, pytestdir] candidates = [pytestpath, pytestdir]
candidates += config.option.rsyncdir candidates += config.option.rsyncdir
rsyncroots = config.getini("rsyncdirs") rsyncroots = config.getini("rsyncdirs")
if rsyncroots: if rsyncroots:
candidates.extend(rsyncroots) candidates.extend(rsyncroots)
roots = [] roots = []
for root in candidates: for root in candidates:
root = py.path.local(root).realpath() root = Path(root).resolve()
if not root.check(): if not root.exists():
raise pytest.UsageError("rsyncdir doesn't exist: %r" % (root,)) raise pytest.UsageError(f"rsyncdir doesn't exist: {root!r}")
if root not in roots: if root not in roots:
roots.append(root) roots.append(root)
return roots return roots
@@ -113,10 +120,13 @@ class NodeManager(object):
def _getrsyncoptions(self): def _getrsyncoptions(self):
"""Get options to be passed for rsync.""" """Get options to be passed for rsync."""
ignores = list(self.DEFAULT_IGNORES) ignores = list(self.DEFAULT_IGNORES)
ignores += self.config.option.rsyncignore ignores += [str(path) for path in self.config.option.rsyncignore]
ignores += self.config.getini("rsyncignore") ignores += [str(path) for path in self.config.getini("rsyncignore")]
return {"ignores": ignores, "verbose": self.config.option.verbose} return {
"ignores": ignores,
"verbose": getattr(self.config.option, "verbose", 0),
}
def rsync(self, gateway, source, notify=None, verbose=False, ignores=None): def rsync(self, gateway, source, notify=None, verbose=False, ignores=None):
"""Perform rsync to remote hosts for node.""" """Perform rsync to remote hosts for node."""
@@ -150,61 +160,80 @@ class NodeManager(object):
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): PathLike = Union[str, "os.PathLike[str]"]
self._synced = {}
self._ignores = []
ignores = kwargs.pop("ignores", None) or []
for x in ignores:
x = getattr(x, "strpath", x)
self._ignores.append(re.compile(fnmatch.translate(x)))
super(HostRSync, self).__init__(sourcedir=sourcedir, **kwargs)
def filter(self, path): def __init__(
path = py.path.local(path) self,
sourcedir: PathLike,
*,
ignores: Optional[Sequence[PathLike]] = None,
**kwargs: object
) -> None:
if ignores is None:
ignores = []
self._ignores = [re.compile(fnmatch.translate(os.fspath(x))) for x in ignores]
super().__init__(sourcedir=Path(sourcedir), **kwargs)
def filter(self, path: PathLike) -> bool:
path = Path(path)
for cre in self._ignores: for cre in self._ignores:
if cre.match(path.basename) or cre.match(path.strpath): if cre.match(path.name) or cre.match(str(path)):
return False return False
else: else:
return True return True
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( super().add_target(gateway, remotepath, finishedcallback=finished, delete=True)
gateway, remotepath, finishedcallback=finished, 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 > 0:
path = os.path.basename(self._sourcedir) + "/" + modified_rel_path path = os.path.basename(self._sourcedir) + "/" + modified_rel_path
remotepath = gateway.spec.chdir remotepath = gateway.spec.chdir
print("%s:%s <= %s" % (gateway.spec, remotepath, path)) print(f"{gateway.spec}:{remotepath} <= {path}")
def make_reltoroot(roots, args): def make_reltoroot(roots: Sequence[Path], args: List[str]) -> List[str]:
# XXX introduce/use public API for splitting pytest 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 = Path(parts[0])
try:
exists = fspath.exists()
except OSError:
exists = False
if not exists:
result.append(arg)
continue
for root in roots: for root in roots:
x = fspath.relto(root) x: Optional[Path]
try:
x = fspath.relative_to(root)
except ValueError:
x = None
if x or fspath == root: if x or fspath == root:
parts[0] = root.basename + "/" + x parts[0] = root.name + "/" + str(x)
break break
else: else:
raise ValueError("arg %s not relative to an rsync root" % (arg,)) raise ValueError(f"arg {arg} not relative to an rsync root")
result.append(splitcode.join(parts)) result.append(splitcode.join(parts))
return result return result
class WorkerController(object): class WorkerController:
ENDMARK = -1 ENDMARK = -1
class RemoteHook:
@pytest.hookimpl(trylast=True)
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
@@ -212,20 +241,15 @@ class WorkerController(object):
self.workerinput = { self.workerinput = {
"workerid": gateway.id, "workerid": gateway.id,
"workercount": len(nodemanager.specs), "workercount": len(nodemanager.specs),
"slaveid": gateway.id, "testrunuid": nodemanager.testrunuid,
"slavecount": len(nodemanager.specs),
"mainargv": sys.argv, "mainargv": sys.argv,
} }
# TODO: deprecated name, backward compatibility only. Remove it in future
self.slaveinput = self.workerinput
self._down = False self._down = False
self._shutdown_sent = False self._shutdown_sent = False
self.log = py.log.Producer("workerctl-%s" % gateway.id) self.log = Producer(f"workerctl-{gateway.id}", enabled=config.option.debug)
if not self.config.option.debug:
py.log.setconsumer(self.log._keywords, None)
def __repr__(self): def __repr__(self):
return "<%s %s>" % (self.__class__.__name__, self.gateway.id) return f"<{self.__class__.__name__} {self.gateway.id}>"
@property @property
def shutting_down(self): def shutting_down(self):
@@ -234,18 +258,28 @@ 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, "_tmp_path_factory"):
basetemp = self.config._tmpdirhandler.getbasetemp() basetemp = self.config._tmp_path_factory.getbasetemp()
option_dict["basetemp"] = str(basetemp.join(name)) option_dict["basetemp"] = str(basetemp / 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
# restore sys.path from a frozen copy for local workers
change_sys_path = _sys_path if self.gateway.spec.popen else None
self.channel.send((self.workerinput, args, option_dict, change_sys_path))
if self.putevent: if self.putevent:
self.channel.setcallback(self.process_from_remote, endmarker=self.ENDMARK) self.channel.setcallback(self.process_from_remote, endmarker=self.ENDMARK)
@@ -270,17 +304,17 @@ class WorkerController(object):
if not self._down: if not self._down:
try: try:
self.sendcommand("shutdown") self.sendcommand("shutdown")
except (IOError, OSError): except 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(f"sending command {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(f"queuing {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,18 +336,22 @@ 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(f"ignoring {eventname}({kwargs})")
elif eventname == "workerready": elif eventname == "workerready":
self.notify_inproc(eventname, node=self, **kwargs) self.notify_inproc(eventname, node=self, **kwargs)
elif eventname == "internal_error":
self.notify_inproc(eventname, node=self, **kwargs)
elif eventname == "workerfinished": elif eventname == "workerfinished":
self._down = True self._down = True
self.workeroutput = self.slaveoutput = kwargs["workeroutput"] self.workeroutput = 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 ("testreport", "collectreport", "teardownreport"): elif eventname in ("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)
@@ -339,92 +377,32 @@ class WorkerController(object):
when=kwargs["when"], when=kwargs["when"],
item=kwargs["item"], item=kwargs["item"],
) )
elif eventname == "warning_recorded":
warning_message = unserialize_warning_message(
kwargs["warning_message_data"]
)
self.notify_inproc(
eventname,
warning_message=warning_message,
when=kwargs["when"],
nodeid=kwargs["nodeid"],
location=kwargs["location"],
)
else: else:
raise ValueError("unknown event: %s" % (eventname,)) raise ValueError(f"unknown event: {eventname}")
except KeyboardInterrupt: except KeyboardInterrupt:
# should not land in receiver-thread # should not land in receiver-thread
raise raise
except: # noqa except: # noqa
from _pytest._code import ExceptionInfo from _pytest._code import ExceptionInfo
# ExceptionInfo API changed in pytest 4.1
if hasattr(ExceptionInfo, "from_current"):
excinfo = ExceptionInfo.from_current() excinfo = ExceptionInfo.from_current()
else:
excinfo = ExceptionInfo()
print("!" * 20, excinfo) 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 assembled_report(reportdict):
from _pytest._code.code import (
ReprEntry,
ReprEntryNative,
ReprExceptionInfo,
ReprFileLocation,
ReprFuncArgs,
ReprLocals,
ReprTraceback,
)
if reportdict["longrepr"]:
if (
"reprcrash" in reportdict["longrepr"]
and "reprtraceback" in reportdict["longrepr"]
):
reprtraceback = reportdict["longrepr"]["reprtraceback"]
reprcrash = reportdict["longrepr"]["reprcrash"]
unserialized_entries = []
reprentry = None
for entry_data in reprtraceback["reprentries"]:
data = entry_data["data"]
entry_type = entry_data["type"]
if entry_type == "ReprEntry":
reprfuncargs = None
reprfileloc = None
reprlocals = None
if data["reprfuncargs"]:
reprfuncargs = ReprFuncArgs(**data["reprfuncargs"])
if data["reprfileloc"]:
reprfileloc = ReprFileLocation(**data["reprfileloc"])
if data["reprlocals"]:
reprlocals = ReprLocals(data["reprlocals"]["lines"])
reprentry = ReprEntry(
lines=data["lines"],
reprfuncargs=reprfuncargs,
reprlocals=reprlocals,
filelocrepr=reprfileloc,
style=data["style"],
)
elif entry_type == "ReprEntryNative":
reprentry = ReprEntryNative(data["lines"])
else:
report_unserialization_failure(entry_type, name, reportdict)
unserialized_entries.append(reprentry)
reprtraceback["reprentries"] = unserialized_entries
exception_info = ReprExceptionInfo(
reprtraceback=ReprTraceback(**reprtraceback),
reprcrash=ReprFileLocation(**reprcrash),
)
for section in reportdict["longrepr"]["sections"]:
exception_info.addsection(*section)
reportdict["longrepr"] = exception_info
return reportdict
if name == "testreport":
return runner.TestReport(**assembled_report(reportdict))
elif name == "collectreport":
return runner.CollectReport(**assembled_report(reportdict))
def unserialize_warning_message(data): def unserialize_warning_message(data):
import warnings import warnings
import importlib import importlib
@@ -459,23 +437,9 @@ def unserialize_warning_message(data):
kwargs = {"message": message, "category": category} kwargs = {"message": message, "category": category}
# access private _WARNING_DETAILS because the attributes vary between Python versions # access private _WARNING_DETAILS because the attributes vary between Python versions
for attr_name in warnings.WarningMessage._WARNING_DETAILS: for attr_name in warnings.WarningMessage._WARNING_DETAILS: # type: ignore[attr-defined]
if attr_name in ("message", "category"): if attr_name in ("message", "category"):
continue continue
kwargs[attr_name] = data[attr_name] kwargs[attr_name] = data[attr_name]
return warnings.WarningMessage(**kwargs) return warnings.WarningMessage(**kwargs) # type: ignore[arg-type]
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,37 +1,30 @@
import py
import pytest
import execnet import execnet
import pytest
import shutil
@pytest.fixture(scope="session", autouse=True) from typing import List
def _ensure_imports():
# we import some modules because pytest-2.8's testdir fixture
# will unload all modules after each test and this cause
# (unknown) problems with execnet.Group()
execnet.Group
execnet.makegateway
pytest_plugins = "pytester" pytest_plugins = "pytester"
# rsyncdirs = ['.', '../xdist', py.path.local(execnet.__file__).dirpath()]
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def _divert_atexit(request, monkeypatch): def _divert_atexit(request, monkeypatch: pytest.MonkeyPatch):
import atexit import atexit
finalizers = [] finalizers = []
def finish(): def fake_register(func, *args, **kwargs):
finalizers.append((func, args, kwargs))
monkeypatch.setattr(atexit, "register", fake_register)
yield
while finalizers: while finalizers:
finalizers.pop()() func, args, kwargs = finalizers.pop()
func(*args, **kwargs)
monkeypatch.setattr(atexit, "register", finalizers.append)
request.addfinalizer(finish)
def pytest_addoption(parser): def pytest_addoption(parser) -> None:
parser.addoption( parser.addoption(
"--gx", "--gx",
action="append", action="append",
@@ -41,36 +34,28 @@ def pytest_addoption(parser):
@pytest.fixture @pytest.fixture
def specssh(request): def specssh(request) -> str:
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) -> List[execnet.XSpec]:
return [execnet.XSpec(spec) for spec in config.getvalueorskip("gspecs")] return [execnet.XSpec(spec) for spec in config.getvalueorskip("gspecs")]
def getspecssh(config): def getspecssh(config) -> str: # type: ignore[return]
xspecs = getgspecs(config) xspecs = getgspecs(config)
for spec in xspecs: for spec in xspecs:
if spec.ssh: if spec.ssh:
if not py.path.local.sysfind("ssh"): if not shutil.which("ssh"):
py.test.skip("command not found: ssh") pytest.skip("command not found: ssh")
return str(spec) return str(spec)
py.test.skip("need '--gx ssh=...'") pytest.skip("need '--gx ssh=...'")
def getsocketspec(config): def getsocketspec(config) -> execnet.XSpec:
xspecs = getgspecs(config) xspecs = getgspecs(config)
for spec in xspecs: for spec in xspecs:
if spec.socket: if spec.socket:
return spec return spec
py.test.skip("need '--gx socket=...'") pytest.skip("need '--gx socket=...'")

View File

@@ -1,59 +1,44 @@
from xdist.dsession import DSession from xdist.dsession import DSession, get_default_max_worker_restart
from xdist.report import report_collection_diff from xdist.report import report_collection_diff
from xdist.scheduler import EachScheduling, LoadScheduling from xdist.scheduler import EachScheduling, LoadScheduling
from typing import Optional
import py
import pytest import pytest
import execnet import execnet
XSpec = execnet.XSpec
def run(item, node, excinfo=None):
runner = item.config.pluginmanager.getplugin("runner")
rep = runner.ItemTestReport(item=item, excinfo=excinfo, when="call")
rep.node = node
return rep
class MockGateway: class MockGateway:
_count = 0 def __init__(self) -> None:
self._count = 0
def __init__(self):
self.id = str(self._count) self.id = str(self._count)
self._count += 1 self._count += 1
class MockNode: class MockNode:
def __init__(self): def __init__(self) -> None:
self.sent = [] self.sent = [] # type: ignore[var-annotated]
self.gateway = MockGateway() self.gateway = MockGateway()
self._shutdown = False self._shutdown = False
def send_runtest_some(self, indices): def send_runtest_some(self, indices) -> None:
self.sent.extend(indices) self.sent.extend(indices)
def send_runtest_all(self): def send_runtest_all(self) -> None:
self.sent.append("ALL") self.sent.append("ALL")
def shutdown(self): def shutdown(self) -> None:
self._shutdown = True self._shutdown = True
@property @property
def shutting_down(self): def shutting_down(self) -> bool:
return self._shutdown return self._shutdown
def dumpqueue(queue):
while queue.qsize():
print(queue.get())
class TestEachScheduling: class TestEachScheduling:
def test_schedule_load_simple(self, testdir): def test_schedule_load_simple(self, pytester: pytest.Pytester) -> None:
node1 = MockNode() node1 = MockNode()
node2 = MockNode() node2 = MockNode()
config = testdir.parseconfig("--tx=2*popen") config = pytester.parseconfig("--tx=2*popen")
sched = EachScheduling(config) sched = EachScheduling(config)
sched.add_node(node1) sched.add_node(node1)
sched.add_node(node2) sched.add_node(node2)
@@ -74,9 +59,9 @@ class TestEachScheduling:
sched.mark_test_complete(node2, 0) sched.mark_test_complete(node2, 0)
assert sched.tests_finished assert sched.tests_finished
def test_schedule_remove_node(self, testdir): def test_schedule_remove_node(self, pytester: pytest.Pytester) -> None:
node1 = MockNode() node1 = MockNode()
config = testdir.parseconfig("--tx=popen") config = pytester.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"]
@@ -93,8 +78,8 @@ class TestEachScheduling:
class TestLoadScheduling: class TestLoadScheduling:
def test_schedule_load_simple(self, testdir): def test_schedule_load_simple(self, pytester: pytest.Pytester) -> None:
config = testdir.parseconfig("--tx=2*popen") config = pytester.parseconfig("--tx=2*popen")
sched = LoadScheduling(config) sched = LoadScheduling(config)
sched.add_node(MockNode()) sched.add_node(MockNode())
sched.add_node(MockNode()) sched.add_node(MockNode())
@@ -117,8 +102,8 @@ class TestLoadScheduling:
sched.mark_test_complete(node1, node1.sent[0]) sched.mark_test_complete(node1, node1.sent[0])
assert sched.tests_finished assert sched.tests_finished
def test_schedule_batch_size(self, testdir): def test_schedule_batch_size(self, pytester: pytest.Pytester) -> None:
config = testdir.parseconfig("--tx=2*popen") config = pytester.parseconfig("--tx=2*popen")
sched = LoadScheduling(config) sched = LoadScheduling(config)
sched.add_node(MockNode()) sched.add_node(MockNode())
sched.add_node(MockNode()) sched.add_node(MockNode())
@@ -130,22 +115,22 @@ class TestLoadScheduling:
# assert not sched.tests_finished # assert not sched.tests_finished
sent1 = node1.sent sent1 = node1.sent
sent2 = node2.sent sent2 = node2.sent
assert sent1 == [0, 2] assert sent1 == [0, 1]
assert sent2 == [1, 3] assert sent2 == [2, 3]
assert sched.pending == [4, 5] assert sched.pending == [4, 5]
assert sched.node2pending[node1] == sent1 assert sched.node2pending[node1] == sent1
assert sched.node2pending[node2] == sent2 assert sched.node2pending[node2] == sent2
assert len(sched.pending) == 2 assert len(sched.pending) == 2
sched.mark_test_complete(node1, 0) sched.mark_test_complete(node1, 0)
assert node1.sent == [0, 2, 4] assert node1.sent == [0, 1, 4]
assert sched.pending == [5] assert sched.pending == [5]
assert node2.sent == [1, 3] assert node2.sent == [2, 3]
sched.mark_test_complete(node1, 2) sched.mark_test_complete(node1, 1)
assert node1.sent == [0, 2, 4, 5] assert node1.sent == [0, 1, 4, 5]
assert not sched.pending assert not sched.pending
def test_schedule_fewer_tests_than_nodes(self, testdir): def test_schedule_fewer_tests_than_nodes(self, pytester: pytest.Pytester) -> None:
config = testdir.parseconfig("--tx=2*popen") config = pytester.parseconfig("--tx=2*popen")
sched = LoadScheduling(config) sched = LoadScheduling(config)
sched.add_node(MockNode()) sched.add_node(MockNode())
sched.add_node(MockNode()) sched.add_node(MockNode())
@@ -164,8 +149,10 @@ class TestLoadScheduling:
assert sent3 == [] assert sent3 == []
assert not sched.pending assert not sched.pending
def test_schedule_fewer_than_two_tests_per_node(self, testdir): def test_schedule_fewer_than_two_tests_per_node(
config = testdir.parseconfig("--tx=2*popen") self, pytester: pytest.Pytester
) -> None:
config = pytester.parseconfig("--tx=2*popen")
sched = LoadScheduling(config) sched = LoadScheduling(config)
sched.add_node(MockNode()) sched.add_node(MockNode())
sched.add_node(MockNode()) sched.add_node(MockNode())
@@ -184,9 +171,9 @@ class TestLoadScheduling:
assert sent3 == [2] assert sent3 == [2]
assert not sched.pending assert not sched.pending
def test_add_remove_node(self, testdir): def test_add_remove_node(self, pytester: pytest.Pytester) -> None:
node = MockNode() node = MockNode()
config = testdir.parseconfig("--tx=popen") config = pytester.parseconfig("--tx=popen")
sched = LoadScheduling(config) sched = LoadScheduling(config)
sched.add_node(node) sched.add_node(node)
collection = ["test_file.py::test_func"] collection = ["test_file.py::test_func"]
@@ -197,13 +184,13 @@ class TestLoadScheduling:
crashitem = sched.remove_node(node) crashitem = sched.remove_node(node)
assert crashitem == collection[0] assert crashitem == collection[0]
def test_different_tests_collected(self, testdir): def test_different_tests_collected(self, pytester: pytest.Pytester) -> None:
""" """
Test that LoadScheduling is reporting collection errors when Test that LoadScheduling is reporting collection errors when
different test ids are collected by workers. different test ids are collected by workers.
""" """
class CollectHook(object): class CollectHook:
""" """
Dummy hook that stores collection reports. Dummy hook that stores collection reports.
""" """
@@ -215,7 +202,7 @@ class TestLoadScheduling:
self.reports.append(report) self.reports.append(report)
collect_hook = CollectHook() collect_hook = CollectHook()
config = testdir.parseconfig("--tx=2*popen") config = pytester.parseconfig("--tx=2*popen")
config.pluginmanager.register(collect_hook, "collect_hook") config.pluginmanager.register(collect_hook, "collect_hook")
node1 = MockNode() node1 = MockNode()
node2 = MockNode() node2 = MockNode()
@@ -231,9 +218,9 @@ class TestLoadScheduling:
class TestDistReporter: class TestDistReporter:
@py.test.mark.xfail @pytest.mark.xfail
def test_rsync_printing(self, testdir, linecomp): def test_rsync_printing(self, pytester: pytest.Pytester, linecomp) -> None:
config = testdir.parseconfig() config = pytester.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)
@@ -258,17 +245,35 @@ class TestDistReporter:
# linecomp.assert_contains_lines([ # linecomp.assert_contains_lines([
# "*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]) # type: ignore[attr-defined]
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() -> None:
"""Test reporting of equal collections.""" """Test reporting of equal collections."""
from_collection = to_collection = ["aaa", "bbb", "ccc"] from_collection = to_collection = ["aaa", "bbb", "ccc"]
assert report_collection_diff(from_collection, to_collection, 1, 2) is None assert report_collection_diff(from_collection, to_collection, 1, 2) is None
def test_report_collection_diff_different(): def test_default_max_worker_restart() -> None:
class config:
class option:
maxworkerrestart: Optional[str] = None
numprocesses: int = 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() -> None:
"""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"]
@@ -285,7 +290,8 @@ def test_report_collection_diff_different():
" bbb\n" " bbb\n"
"+XXX\n" "+XXX\n"
" ccc\n" " ccc\n"
"-YYY" "-YYY\n"
"To see why this happens see Known limitations in documentation"
) )
msg = report_collection_diff(from_collection, to_collection, "1", "2") msg = report_collection_diff(from_collection, to_collection, "1", "2")
@@ -293,8 +299,8 @@ def test_report_collection_diff_different():
@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(pytester: pytest.Pytester) -> None:
testdir.makepyfile( pytester.makepyfile(
""" """
import pytest import pytest
@@ -303,6 +309,6 @@ def test_pytest_issue419(testdir):
pass pass
""" """
) )
reprec = testdir.inline_run("-n1") reprec = pytester.inline_run("-n1")
reprec.assertoutcome(passed=2) reprec.assertoutcome(passed=2)
assert 0 assert 0

View File

@@ -1,89 +1,108 @@
import py import unittest.mock
from typing import List
import pytest import pytest
from pkg_resources import parse_version import shutil
import textwrap
from pathlib import Path
from xdist.looponfail import RemoteControl from xdist.looponfail import RemoteControl
from xdist.looponfail import StatRecorder from xdist.looponfail import StatRecorder
PYTEST_GTE_7 = hasattr(pytest, "version_tuple") and pytest.version_tuple >= (7, 0) # type: ignore[attr-defined]
class TestStatRecorder: class TestStatRecorder:
def test_filechange(self, tmpdir): def test_filechange(self, tmp_path: Path) -> None:
tmp = tmpdir tmp = tmp_path
hello = tmp.ensure("hello.py") hello = tmp / "hello.py"
hello.touch()
sd = StatRecorder([tmp]) sd = StatRecorder([tmp])
changed = sd.check() changed = sd.check()
assert not changed assert not changed
hello.write("world") hello.write_text("world")
changed = sd.check() changed = sd.check()
assert changed assert changed
(hello + "c").write("hello") hello.with_suffix(".pyc").write_text("hello")
changed = sd.check() changed = sd.check()
assert not changed assert not changed
p = tmp.ensure("new.py") p = tmp / "new.py"
p.touch()
changed = sd.check() changed = sd.check()
assert changed assert changed
p.remove() p.unlink()
changed = sd.check() changed = sd.check()
assert changed assert changed
tmp.join("a", "b", "c.py").ensure() tmp.joinpath("a", "b").mkdir(parents=True)
tmp.joinpath("a", "b", "c.py").touch()
changed = sd.check() changed = sd.check()
assert changed assert changed
tmp.join("a", "c.txt").ensure() tmp.joinpath("a", "c.txt").touch()
changed = sd.check() changed = sd.check()
assert changed assert changed
changed = sd.check() changed = sd.check()
assert not changed assert not changed
tmp.join("a").remove() shutil.rmtree(str(tmp.joinpath("a")))
changed = sd.check() changed = sd.check()
assert changed assert changed
def test_dirchange(self, tmpdir): def test_dirchange(self, tmp_path: Path) -> None:
tmp = tmpdir tmp = tmp_path
tmp.ensure("dir", "hello.py") tmp.joinpath("dir").mkdir()
tmp.joinpath("dir", "hello.py").touch()
sd = StatRecorder([tmp]) sd = StatRecorder([tmp])
assert not sd.fil(tmp.join("dir")) assert not sd.fil(tmp / "dir")
def test_filechange_deletion_race(self, tmpdir, monkeypatch): def test_filechange_deletion_race(self, tmp_path: Path) -> None:
tmp = tmpdir tmp = tmp_path
sd = StatRecorder([tmp]) sd = StatRecorder([tmp])
changed = sd.check() changed = sd.check()
assert not changed assert not changed
p = tmp.ensure("new.py") p = tmp.joinpath("new.py")
p.touch()
changed = sd.check() changed = sd.check()
assert changed assert changed
p.remove() p.unlink()
# 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]) dirname = str(tmp)
dirnames: List[str] = []
filenames = [str(p)]
with unittest.mock.patch(
"os.walk", return_value=[(dirname, dirnames, filenames)], autospec=True
):
changed = sd.check() changed = sd.check()
assert changed assert changed
def test_pycremoval(self, tmpdir): def test_pycremoval(self, tmp_path: Path) -> None:
tmp = tmpdir tmp = tmp_path
hello = tmp.ensure("hello.py") hello = tmp / "hello.py"
hello.touch()
sd = StatRecorder([tmp]) sd = StatRecorder([tmp])
changed = sd.check() changed = sd.check()
assert not changed assert not changed
pycfile = hello + "c" pycfile = hello.with_suffix(".pyc")
pycfile.ensure() pycfile.touch()
hello.write("world") hello.write_text("world")
changed = sd.check() changed = sd.check()
assert changed assert changed
assert not pycfile.check() assert not pycfile.exists()
def test_waitonchange(self, tmpdir, monkeypatch): def test_waitonchange(
tmp = tmpdir self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
tmp = tmp_path
sd = StatRecorder([tmp]) sd = StatRecorder([tmp])
ret_values = [True, False] ret_values = [True, False]
@@ -93,37 +112,45 @@ class TestStatRecorder:
class TestRemoteControl: class TestRemoteControl:
def test_nofailures(self, testdir): def test_nofailures(self, pytester: pytest.Pytester) -> None:
item = testdir.getitem("def test_func(): pass\n") item = pytester.getitem("def test_func(): pass\n")
control = RemoteControl(item.config) control = RemoteControl(item.config)
control.setup() control.setup()
topdir, failures = control.runsession()[:2] topdir, failures = control.runsession()[:2]
assert not failures assert not failures
def test_failures_somewhere(self, testdir): def test_failures_somewhere(self, pytester: pytest.Pytester) -> None:
item = testdir.getitem("def test_func():\n assert 0\n") item = pytester.getitem("def test_func():\n assert 0\n")
control = RemoteControl(item.config) control = RemoteControl(item.config)
control.setup() control.setup()
failures = control.runsession() failures = control.runsession()
assert failures assert failures
control.setup() control.setup()
item.fspath.write("def test_func():\n assert 1\n") item_path = item.path if PYTEST_GTE_7 else Path(str(item.fspath)) # type: ignore[attr-defined]
removepyc(item.fspath) item_path.write_text("def test_func():\n assert 1\n")
removepyc(item_path)
topdir, failures = control.runsession()[:2] topdir, failures = control.runsession()[:2]
assert not failures assert not failures
def test_failure_change(self, testdir): def test_failure_change(self, pytester: pytest.Pytester) -> None:
modcol = testdir.getitem( modcol = pytester.getitem(
textwrap.dedent(
""" """
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( if PYTEST_GTE_7:
py.code.Source( modcol_path = modcol.path # type:ignore[attr-defined]
else:
modcol_path = Path(str(modcol.fspath))
modcol_path.write_text(
textwrap.dedent(
""" """
def test_func(): def test_func():
assert 1 assert 1
@@ -132,24 +159,31 @@ class TestRemoteControl:
""" """
) )
) )
removepyc(modcol.fspath) removepyc(modcol_path)
control.loop_once() control.loop_once()
assert not control.failures assert not control.failures
control.loop_once() control.loop_once()
assert control.failures assert control.failures
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(
modcol = testdir.getitem( self, pytester: pytest.Pytester, monkeypatch: pytest.MonkeyPatch
) -> None:
modcol = pytester.getitem(
textwrap.dedent(
""" """
def test_func(): def test_func():
assert 0 assert 0
""" """
) )
parent = modcol.fspath.dirpath().dirpath() )
parent.chdir() if PYTEST_GTE_7:
parent = modcol.path.parent.parent # type: ignore[attr-defined]
else:
parent = Path(modcol.fspath.dirpath().dirpath())
monkeypatch.chdir(parent)
modcol.config.args = [ modcol.config.args = [
py.path.local(x).relto(parent) for x in modcol.config.args str(Path(x).relative_to(parent)) for x in modcol.config.args
] ]
control = RemoteControl(modcol.config) control = RemoteControl(modcol.config)
control.loop_once() control.loop_once()
@@ -159,8 +193,9 @@ class TestRemoteControl:
class TestLooponFailing: class TestLooponFailing:
def test_looponfail_from_fail_to_ok(self, testdir): def test_looponfail_from_fail_to_ok(self, pytester: pytest.Pytester) -> None:
modcol = testdir.getmodulecol( modcol = pytester.getmodulecol(
textwrap.dedent(
""" """
def test_one(): def test_one():
x = 0 x = 0
@@ -169,12 +204,14 @@ class TestLooponFailing:
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( modcol_path = modcol.path if PYTEST_GTE_7 else Path(modcol.fspath)
py.code.Source( modcol_path.write_text(
textwrap.dedent(
""" """
def test_one(): def test_one():
assert 1 assert 1
@@ -183,24 +220,27 @@ class TestLooponFailing:
""" """
) )
) )
removepyc(modcol.fspath) removepyc(modcol_path)
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, pytester: pytest.Pytester) -> None:
modcol = testdir.getmodulecol( modcol = pytester.getmodulecol(
textwrap.dedent(
""" """
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( modcol_path = modcol.path if PYTEST_GTE_7 else Path(modcol.fspath)
py.code.Source( modcol_path.write_text(
textwrap.dedent(
""" """
def test_one(): def test_one():
assert 1 # passes now assert 1 # passes now
@@ -209,7 +249,7 @@ class TestLooponFailing:
""" """
) )
) )
removepyc(modcol.fspath) removepyc(modcol_path)
remotecontrol.loop_once() remotecontrol.loop_once()
assert len(remotecontrol.failures) == 0 assert len(remotecontrol.failures) == 0
remotecontrol.loop_once() remotecontrol.loop_once()
@@ -217,13 +257,10 @@ class TestLooponFailing:
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]
@pytest.mark.xfail( @pytest.mark.xfail(reason="broken by pytest 3.1+", strict=True)
parse_version(pytest.__version__) >= parse_version("3.1"), def test_looponfail_removed_test(self, pytester: pytest.Pytester) -> None:
reason="broken by pytest 3.1+", modcol = pytester.getmodulecol(
strict=True, textwrap.dedent(
)
def test_looponfail_removed_test(self, testdir):
modcol = testdir.getmodulecol(
""" """
def test_one(): def test_one():
assert 0 assert 0
@@ -231,12 +268,13 @@ class TestLooponFailing:
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( modcol.path.write_text(
py.code.Source( textwrap.dedent(
""" """
def test_xxx(): # renamed test def test_xxx(): # renamed test
assert 0 assert 0
@@ -245,20 +283,24 @@ class TestLooponFailing:
""" """
) )
) )
removepyc(modcol.fspath) removepyc(modcol.path)
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
def test_looponfail_multiple_errors(self, testdir, monkeypatch): def test_looponfail_multiple_errors(
modcol = testdir.getmodulecol( self, pytester: pytest.Pytester, monkeypatch: pytest.MonkeyPatch
) -> None:
modcol = pytester.getmodulecol(
textwrap.dedent(
""" """
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
@@ -274,55 +316,59 @@ class TestLooponFailing:
class TestFunctional: class TestFunctional:
def test_fail_to_ok(self, testdir): def test_fail_to_ok(self, pytester: pytest.Pytester) -> None:
p = testdir.makepyfile( p = pytester.makepyfile(
textwrap.dedent(
""" """
def test_one(): def test_one():
x = 0 x = 0
assert x == 1 assert x == 1
""" """
) )
# p = testdir.mkdir("sub").join(p1.basename) )
# p = pytester.mkdir("sub").join(p1.basename)
# p1.move(p) # p1.move(p)
child = testdir.spawn_pytest("-f %s --traceconfig" % p) child = pytester.spawn_pytest("-f %s --traceconfig" % p, expect_timeout=30.0)
child.expect("def test_one") child.expect("def test_one")
child.expect("x == 1") child.expect("x == 1")
child.expect("1 failed") child.expect("1 failed")
child.expect("### LOOPONFAILING ####") child.expect("### LOOPONFAILING ####")
child.expect("waiting for changes") child.expect("waiting for changes")
p.write( p.write_text(
py.code.Source( textwrap.dedent(
""" """
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, pytester: pytest.Pytester) -> None:
p = testdir.makepyfile( p = pytester.makepyfile(
textwrap.dedent(
""" """
import py import pytest
@py.test.mark.xfail @pytest.mark.xfail
def test_one(): def test_one():
pass pass
""" """
) )
child = testdir.spawn_pytest("-f %s" % p) )
child = pytester.spawn_pytest("-f %s" % p, expect_timeout=30.0)
child.expect("1 xpass") child.expect("1 xpass")
# child.expect("### LOOPONFAILING ####") # child.expect("### LOOPONFAILING ####")
child.expect("waiting for changes") child.expect("waiting for changes")
child.kill(15) child.kill(15)
def removepyc(path): def removepyc(path: Path) -> None:
# XXX damn those pyc files # XXX damn those pyc files
pyc = path + "c" pyc = path.with_suffix(".pyc")
if pyc.check(): if pyc.exists():
pyc.remove() pyc.unlink()
c = path.dirpath("__pycache__") c = path.parent / "__pycache__"
if c.check(): if c.exists():
c.remove() shutil.rmtree(c)

View File

@@ -3,8 +3,8 @@ 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, pytester: pytest.Pytester) -> None:
testdir.makepyfile( pytester.makepyfile(
""" """
import os import os
def test_a(): pass def test_a(): pass
@@ -13,25 +13,29 @@ class TestHooks:
""" """
) )
def test_runtest_logreport(self, testdir): def test_runtest_logreport(self, pytester: pytest.Pytester) -> None:
"""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( pytester.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 = pytester.runpytest("-n1", "-s")
res.stdout.fnmatch_lines( res.stdout.fnmatch_lines(
[ [
"*HOOK: test_runtest_logreport.py::test_a gw0 *", "*HOOK: test_runtest_logreport.py::test_a gw0 *",
@@ -41,10 +45,9 @@ class TestHooks:
] ]
) )
def test_node_collection_finished(self, testdir): def test_node_collection_finished(self, pytester: pytest.Pytester) -> None:
"""Test pytest_xdist_node_collection_finished hook (#8). """Test pytest_xdist_node_collection_finished hook (#8)."""
""" pytester.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']
@@ -52,8 +55,67 @@ class TestHooks:
print("HOOK: %s %s" % (workerid, ', '.join(stripped_ids))) print("HOOK: %s %s" % (workerid, ', '.join(stripped_ids)))
""" """
) )
res = testdir.runpytest("-n2", "-s") res = pytester.runpytest("-n2", "-s")
res.stdout.fnmatch_lines_random( res.stdout.fnmatch_lines_random(
["*HOOK: gw0 test_a, test_b, test_c", "*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(["*3 passed*"]) res.stdout.fnmatch_lines(["*3 passed*"])
class TestCrashItem:
@pytest.fixture(autouse=True)
def create_test_file(self, pytester: pytest.Pytester) -> None:
pytester.makepyfile(
"""
import os
def test_a(): pass
def test_b(): os._exit(1)
def test_c(): pass
def test_d(): pass
"""
)
def test_handlecrashitem(self, pytester: pytest.Pytester) -> None:
"""Test pytest_handlecrashitem hook."""
pytester.makeconftest(
"""
test_runs = 0
def pytest_handlecrashitem(crashitem, report, sched):
global test_runs
if test_runs == 0:
sched.mark_test_pending(crashitem)
test_runs = 1
else:
print("HOOK: pytest_handlecrashitem")
"""
)
res = pytester.runpytest("-n2", "-s")
res.stdout.fnmatch_lines_random(["*HOOK: pytest_handlecrashitem"])
res.stdout.fnmatch_lines(["*3 passed*"])
def test_handlecrashitem_one(self, pytester: pytest.Pytester) -> None:
"""Test pytest_handlecrashitem hook with just one test."""
pytester.makeconftest(
"""
test_runs = 0
def pytest_handlecrashitem(crashitem, report, sched):
global test_runs
if test_runs == 0:
sched.mark_test_pending(crashitem)
test_runs = 1
else:
print("HOOK: pytest_handlecrashitem")
"""
)
res = pytester.runpytest("-n1", "-s", "-k", "test_b")
res.stdout.fnmatch_lines_random(["*HOOK: pytest_handlecrashitem"])
res.stdout.fnmatch_lines(
[
"FAILED test_handlecrashitem_one.py::test_b",
"FAILED test_handlecrashitem_one.py::test_b",
]
)

View File

@@ -1,41 +1,63 @@
import py from contextlib import suppress
from pathlib import Path
import sys
import os
import execnet import execnet
from xdist.workermanage import NodeManager from xdist.workermanage import NodeManager
import pytest
def test_dist_incompatibility_messages(testdir):
result = testdir.runpytest("--pdb", "--looponfail") @pytest.fixture
def monkeypatch_3_cpus(monkeypatch: pytest.MonkeyPatch):
"""Make pytest-xdist believe the system has 3 CPUs"""
# block import
monkeypatch.setitem(sys.modules, "psutil", None) # type: ignore
monkeypatch.delattr(os, "sched_getaffinity", raising=False)
monkeypatch.setattr(os, "cpu_count", lambda: 3)
def test_dist_incompatibility_messages(pytester: pytest.Pytester) -> None:
result = pytester.runpytest("--pdb", "--looponfail")
assert result.ret != 0 assert result.ret != 0
result = testdir.runpytest("--pdb", "-n", "3") result = pytester.runpytest("--pdb", "-n", "3")
assert result.ret != 0 assert result.ret != 0
assert "incompatible" in result.stderr.str() assert "incompatible" in result.stderr.str()
result = testdir.runpytest("--pdb", "-d", "--tx", "popen") result = pytester.runpytest("--pdb", "-d", "--tx", "popen")
assert result.ret != 0 assert result.ret != 0
assert "incompatible" in result.stderr.str() assert "incompatible" in result.stderr.str()
def test_dist_options(testdir): def test_dist_options(pytester: pytest.Pytester) -> None:
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 = pytester.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 = pytester.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") config = pytester.parseconfigure("--numprocesses", "3", "--maxprocesses", "2")
check_options(config) check_options(config)
assert config.option.dist == "load" assert config.option.dist == "load"
assert config.option.tx == ["popen"] * 2 assert config.option.tx == ["popen"] * 2
config = testdir.parseconfigure("-d") config = pytester.parseconfigure("-d")
check_options(config) check_options(config)
assert config.option.dist == "load" assert config.option.dist == "load"
def test_auto_detect_cpus(testdir, monkeypatch): def test_auto_detect_cpus(
import os pytester: pytest.Pytester, monkeypatch: pytest.MonkeyPatch
) -> None:
from xdist.plugin import pytest_cmdline_main as check_options
with suppress(ImportError):
import psutil
monkeypatch.setattr(psutil, "cpu_count", lambda logical=True: None)
if hasattr(os, "sched_getaffinity"): if hasattr(os, "sched_getaffinity"):
monkeypatch.setattr(os, "sched_getaffinity", lambda _pid: set(range(99))) monkeypatch.setattr(os, "sched_getaffinity", lambda _pid: set(range(99)))
@@ -46,52 +68,213 @@ def test_auto_detect_cpus(testdir, monkeypatch):
monkeypatch.setattr(multiprocessing, "cpu_count", lambda: 99) monkeypatch.setattr(multiprocessing, "cpu_count", lambda: 99)
config = testdir.parseconfigure("-n2") config = pytester.parseconfigure("-n2")
assert config.getoption("numprocesses") == 2 assert config.getoption("numprocesses") == 2
config = testdir.parseconfigure("-nauto") config = pytester.parseconfigure("-nauto")
check_options(config)
assert config.getoption("numprocesses") == 99 assert config.getoption("numprocesses") == 99
config = pytester.parseconfigure("-nauto", "--pdb")
check_options(config)
assert config.getoption("usepdb")
assert config.getoption("numprocesses") == 0
assert config.getoption("dist") == "no"
config = pytester.parseconfigure("-nlogical", "--pdb")
check_options(config)
assert config.getoption("usepdb")
assert config.getoption("numprocesses") == 0
assert config.getoption("dist") == "no"
monkeypatch.delattr(os, "sched_getaffinity", raising=False) monkeypatch.delattr(os, "sched_getaffinity", raising=False)
monkeypatch.setenv("TRAVIS", "true") monkeypatch.setenv("TRAVIS", "true")
config = testdir.parseconfigure("-nauto") config = pytester.parseconfigure("-nauto")
check_options(config)
assert config.getoption("numprocesses") == 2 assert config.getoption("numprocesses") == 2
def test_boxed_with_collect_only(testdir): def test_auto_detect_cpus_psutil(
pytester: pytest.Pytester, monkeypatch: pytest.MonkeyPatch
) -> None:
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") psutil = pytest.importorskip("psutil")
monkeypatch.setattr(psutil, "cpu_count", lambda logical=True: 84 if logical else 42)
config = pytester.parseconfigure("-nauto")
check_options(config) check_options(config)
assert config.option.forked assert config.getoption("numprocesses") == 42
config = testdir.parseconfigure("-n1", "--collect-only") config = pytester.parseconfigure("-nlogical")
check_options(config) check_options(config)
assert not config.option.forked assert config.getoption("numprocesses") == 84
config = testdir.parseconfigure("-n1", "--boxed", "--collect-only")
def test_auto_detect_cpus_os(
pytester: pytest.Pytester, monkeypatch: pytest.MonkeyPatch, monkeypatch_3_cpus
) -> None:
from xdist.plugin import pytest_cmdline_main as check_options
config = pytester.parseconfigure("-nauto")
check_options(config) check_options(config)
assert config.option.forked assert config.getoption("numprocesses") == 3
config = pytester.parseconfigure("-nlogical")
check_options(config)
assert config.getoption("numprocesses") == 3
def test_dsession_with_collect_only(testdir): def test_hook_auto_num_workers(
pytester: pytest.Pytester, monkeypatch: pytest.MonkeyPatch
) -> None:
from xdist.plugin import pytest_cmdline_main as check_options
pytester.makeconftest(
"""
def pytest_xdist_auto_num_workers():
return 42
"""
)
config = pytester.parseconfigure("-nauto")
check_options(config)
assert config.getoption("numprocesses") == 42
config = pytester.parseconfigure("-nlogical")
check_options(config)
assert config.getoption("numprocesses") == 42
def test_hook_auto_num_workers_arg(
pytester: pytest.Pytester, monkeypatch: pytest.MonkeyPatch
) -> None:
# config.option.numprocesses is a pytest feature,
# but we document it so let's test it.
from xdist.plugin import pytest_cmdline_main as check_options
pytester.makeconftest(
"""
def pytest_xdist_auto_num_workers(config):
if config.option.numprocesses == 'auto':
return 42
if config.option.numprocesses == 'logical':
return 8
"""
)
config = pytester.parseconfigure("-nauto")
check_options(config)
assert config.getoption("numprocesses") == 42
config = pytester.parseconfigure("-nlogical")
check_options(config)
assert config.getoption("numprocesses") == 8
def test_hook_auto_num_workers_none(
pytester: pytest.Pytester, monkeypatch: pytest.MonkeyPatch, monkeypatch_3_cpus
) -> None:
# Returning None from a hook to skip it is pytest behavior,
# but we document it so let's test it.
from xdist.plugin import pytest_cmdline_main as check_options
pytester.makeconftest(
"""
def pytest_xdist_auto_num_workers():
return None
"""
)
config = pytester.parseconfigure("-nauto")
check_options(config)
assert config.getoption("numprocesses") == 3
monkeypatch.setenv("PYTEST_XDIST_AUTO_NUM_WORKERS", "5")
config = pytester.parseconfigure("-nauto")
check_options(config)
assert config.getoption("numprocesses") == 5
def test_envvar_auto_num_workers(
pytester: pytest.Pytester, monkeypatch: pytest.MonkeyPatch
) -> None:
from xdist.plugin import pytest_cmdline_main as check_options
monkeypatch.setenv("PYTEST_XDIST_AUTO_NUM_WORKERS", "7")
config = pytester.parseconfigure("-nauto")
check_options(config)
assert config.getoption("numprocesses") == 7
config = pytester.parseconfigure("-nlogical")
check_options(config)
assert config.getoption("numprocesses") == 7
def test_envvar_auto_num_workers_warn(
pytester: pytest.Pytester, monkeypatch: pytest.MonkeyPatch, monkeypatch_3_cpus
) -> None:
from xdist.plugin import pytest_cmdline_main as check_options
monkeypatch.setenv("PYTEST_XDIST_AUTO_NUM_WORKERS", "fourscore")
config = pytester.parseconfigure("-nauto")
with pytest.warns(UserWarning):
check_options(config)
assert config.getoption("numprocesses") == 3
def test_auto_num_workers_hook_overrides_envvar(
pytester: pytest.Pytester, monkeypatch: pytest.MonkeyPatch, monkeypatch_3_cpus
) -> None:
from xdist.plugin import pytest_cmdline_main as check_options
monkeypatch.setenv("PYTEST_XDIST_AUTO_NUM_WORKERS", "987")
pytester.makeconftest(
"""
def pytest_xdist_auto_num_workers():
return 2
"""
)
config = pytester.parseconfigure("-nauto")
check_options(config)
assert config.getoption("numprocesses") == 2
config = pytester.parseconfigure("-nauto")
check_options(config)
assert config.getoption("numprocesses") == 2
def test_dsession_with_collect_only(pytester: pytest.Pytester) -> None:
from xdist.plugin import pytest_cmdline_main as check_options from xdist.plugin import pytest_cmdline_main as check_options
from xdist.plugin import pytest_configure as configure from xdist.plugin import pytest_configure as configure
config = testdir.parseconfigure("-n1") config = pytester.parseconfigure("-n1")
check_options(config) check_options(config)
configure(config) configure(config)
assert config.pluginmanager.hasplugin("dsession") assert config.pluginmanager.hasplugin("dsession")
config = testdir.parseconfigure("-n1", "--collect-only") config = pytester.parseconfigure("-n1", "--collect-only")
check_options(config) check_options(config)
configure(config) configure(config)
assert not config.pluginmanager.hasplugin("dsession") assert not config.pluginmanager.hasplugin("dsession")
def test_testrunuid_provided(pytester: pytest.Pytester) -> None:
config = pytester.parseconfigure("--testrunuid", "test123", "--tx=popen")
nm = NodeManager(config)
assert nm.testrunuid == "test123"
def test_testrunuid_generated(pytester: pytest.Pytester) -> None:
config = pytester.parseconfigure("--tx=popen")
nm = NodeManager(config)
assert len(nm.testrunuid) == 32
class TestDistOptions: class TestDistOptions:
def test_getxspecs(self, testdir): def test_getxspecs(self, pytester: pytest.Pytester) -> None:
config = testdir.parseconfigure("--tx=popen", "--tx", "ssh=xyz") config = pytester.parseconfigure("--tx=popen", "--tx", "ssh=xyz")
nodemanager = NodeManager(config) nodemanager = NodeManager(config)
xspecs = nodemanager._getxspecs() xspecs = nodemanager._getxspecs()
assert len(xspecs) == 2 assert len(xspecs) == 2
@@ -99,39 +282,39 @@ class TestDistOptions:
assert xspecs[0].popen assert xspecs[0].popen
assert xspecs[1].ssh == "xyz" assert xspecs[1].ssh == "xyz"
def test_xspecs_multiplied(self, testdir): def test_xspecs_multiplied(self, pytester: pytest.Pytester) -> None:
config = testdir.parseconfigure("--tx=3*popen") config = pytester.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, pytester: pytest.Pytester) -> None:
config = testdir.parseconfigure("--rsyncdir=" + str(testdir.tmpdir)) config = pytester.parseconfigure("--rsyncdir=" + str(pytester.path))
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")])
assert nm.roots assert nm.roots
assert testdir.tmpdir in nm.roots assert pytester.path in nm.roots
def test_getrsyncignore(self, testdir): def test_getrsyncignore(self, pytester: pytest.Pytester) -> None:
config = testdir.parseconfigure("--rsyncignore=fo*") config = pytester.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, pytester: pytest.Pytester) -> None:
p = py.path.local() p = Path.cwd()
for bn in "x y z".split(): for bn in ("x", "y", "z"):
p.mkdir(bn) p.joinpath(bn).mkdir()
testdir.makeini( pytester.makeini(
""" """
[pytest] [pytest]
rsyncdirs= x rsyncdirs= x
""" """
) )
config = testdir.parseconfigure(testdir.tmpdir, "--rsyncdir=y", "--rsyncdir=z") config = pytester.parseconfigure(pytester.path, "--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 Path("y").resolve() in roots
assert py.path.local("z") in roots assert Path("z").resolve() in roots
assert testdir.tmpdir.join("x") in roots assert pytester.path.joinpath("x") in roots

View File

@@ -1,14 +1,13 @@
import py
import pprint import pprint
import pytest import pytest
import sys import sys
import uuid
from xdist.workermanage import WorkerController, unserialize_report from xdist.workermanage import WorkerController
from xdist.remote import serialize_report
import execnet import execnet
import marshal import marshal
from six.moves.queue import Queue from queue import Queue
WAIT_TIMEOUT = 10.0 WAIT_TIMEOUT = 10.0
@@ -26,25 +25,25 @@ 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 f"<EventCall {self.name}(**{self.kwargs})>"
class WorkerSetup: class WorkerSetup:
use_callback = False def __init__(self, request, pytester: pytest.Pytester) -> None:
def __init__(self, request, testdir):
self.request = request self.request = request
self.testdir = testdir self.pytester = pytester
self.events = Queue() self.use_callback = False
self.events = Queue() # type: ignore[var-annotated]
def setup(self,): def setup(self) -> None:
self.testdir.chdir() self.pytester.chdir()
# import os ; os.environ['EXECNET_DEBUG'] = "2" # import os ; os.environ['EXECNET_DEBUG'] = "2"
self.gateway = execnet.makegateway() self.gateway = execnet.makegateway()
self.config = config = self.testdir.parseconfigure() self.config = config = self.pytester.parseconfigure()
putevent = self.use_callback and self.events.put or None putevent = self.events.put if self.use_callback else None
class DummyMananger: class DummyMananger:
testrunuid = uuid.uuid4().hex
specs = [0, 1] specs = [0, 1]
self.slp = WorkerController(DummyMananger, self.gateway, config, putevent) self.slp = WorkerController(DummyMananger, self.gateway, config, putevent)
@@ -60,210 +59,41 @@ 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(f"skipping {ev}")
def sendcommand(self, name, **kwargs): def sendcommand(self, name, **kwargs):
self.slp.sendcommand(name, **kwargs) self.slp.sendcommand(name, **kwargs)
@pytest.fixture @pytest.fixture
def worker(request, testdir): def worker(request, pytester: pytest.Pytester) -> WorkerSetup:
return WorkerSetup(request, testdir) return WorkerSetup(request, pytester)
@pytest.mark.xfail(reason="#59") @pytest.mark.xfail(reason="#59")
def test_remoteinitconfig(testdir): def test_remoteinitconfig(pytester: pytest.Pytester) -> None:
from xdist.remote import remote_initconfig from xdist.remote import remote_initconfig
config1 = testdir.parseconfig() config1 = pytester.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: WorkerSetup, unserialize_report
) -> None:
worker.pytester.makepyfile(
""" """
def test_func(): def test_func():
pass pass
@@ -276,7 +106,7 @@ 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"] == str(worker.pytester.path)
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))))
@@ -287,15 +117,15 @@ 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
def test_remote_collect_skip(self, worker): def test_remote_collect_skip(self, worker: WorkerSetup, unserialize_report) -> None:
worker.testdir.makepyfile( worker.pytester.makepyfile(
""" """
import pytest import pytest
pytest.skip("hello", allow_module_level=True) pytest.skip("hello", allow_module_level=True)
@@ -306,26 +136,26 @@ class TestWorkerInteractor:
assert not ev.kwargs assert not ev.kwargs
ev = worker.popevent() ev = worker.popevent()
assert ev.name == "collectreport" assert ev.name == "collectreport"
rep = unserialize_report(ev.name, ev.kwargs["data"]) rep = unserialize_report(ev.kwargs["data"])
assert rep.skipped assert rep.skipped
assert rep.longrepr[2] == "Skipped: hello" 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: WorkerSetup, unserialize_report) -> None:
worker.testdir.makepyfile("""aasd qwe""") worker.pytester.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"
rep = unserialize_report(ev.name, ev.kwargs["data"]) rep = unserialize_report(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: WorkerSetup, unserialize_report) -> None:
worker.testdir.makepyfile( worker.pytester.makepyfile(
""" """
def test_func(): pass def test_func(): pass
def test_func2(): pass def test_func2(): pass
@@ -346,22 +176,24 @@ class TestWorkerInteractor:
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(
py.test.xfail("implement a simple test for event production") self, pytester: pytest.Pytester, worker: WorkerSetup
assert not worker.use_callback ) -> None:
worker.testdir.makepyfile( pytest.xfail("implement a simple test for event production")
assert not worker.use_callback # type: ignore[unreachable]
worker.pytester.makepyfile(
""" """
def test_func(): def test_func():
pass pass
""" """
) )
worker.setup() worker.setup()
hookrec = testdir.getreportrecorder(worker.config) hookrec = pytester.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)
@@ -377,7 +209,9 @@ class TestWorkerInteractor:
] ]
) )
def test_process_from_remote_error_handling(self, worker, capsys): def test_process_from_remote_error_handling(
self, worker: WorkerSetup, capsys: pytest.CaptureFixture[str]
) -> None:
worker.use_callback = True worker.use_callback = True
worker.setup() worker.setup()
worker.slp.process_from_remote(("<nonono>", ())) worker.slp.process_from_remote(("<nonono>", ()))
@@ -387,22 +221,23 @@ class TestWorkerInteractor:
assert ev.name == "errordown" assert ev.name == "errordown"
def test_remote_env_vars(testdir): def test_remote_env_vars(pytester: pytest.Pytester) -> None:
testdir.makepyfile( pytester.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 = pytester.runpytest("-n2", "--max-worker-restart=0")
assert result.ret == 0 assert result.ret == 0
def test_remote_inner_argv(testdir): def test_remote_inner_argv(pytester: pytest.Pytester) -> None:
"""Test/document the behavior due to execnet using `python -c`.""" """Test/document the behavior due to execnet using `python -c`."""
testdir.makepyfile( pytester.makepyfile(
""" """
import sys import sys
@@ -410,14 +245,14 @@ def test_remote_inner_argv(testdir):
assert sys.argv == ["-c"] assert sys.argv == ["-c"]
""" """
) )
result = testdir.runpytest("-n1") result = pytester.runpytest("-n1")
assert result.ret == 0 assert result.ret == 0
def test_remote_mainargv(testdir): def test_remote_mainargv(pytester: pytest.Pytester) -> None:
outer_argv = sys.argv outer_argv = sys.argv
testdir.makepyfile( pytester.makepyfile(
""" """
def test_mainargv(request): def test_mainargv(request):
assert request.config.workerinput["mainargv"] == {!r} assert request.config.workerinput["mainargv"] == {!r}
@@ -425,14 +260,14 @@ def test_remote_mainargv(testdir):
outer_argv outer_argv
) )
) )
result = testdir.runpytest("-n1") result = pytester.runpytest("-n1")
assert result.ret == 0 assert result.ret == 0
def test_remote_usage_prog(testdir, request): def test_remote_usage_prog(pytester: pytest.Pytester, request) -> None:
if not hasattr(request.config._parser, "prog"): if not hasattr(request.config._parser, "prog"):
pytest.skip("prog not available in config parser") pytest.skip("prog not available in config parser")
testdir.makeconftest( pytester.makeconftest(
""" """
import pytest import pytest
@@ -447,7 +282,7 @@ def test_remote_usage_prog(testdir, request):
config_parser = config._parser config_parser = config._parser
""" """
) )
testdir.makepyfile( pytester.makepyfile(
""" """
import sys import sys
@@ -456,8 +291,20 @@ def test_remote_usage_prog(testdir, request):
""" """
) )
result = testdir.runpytest_subprocess("-n1") result = pytester.runpytest_subprocess("-n1")
assert result.ret == 1 assert result.ret == 1
result.stdout.fnmatch_lines( result.stdout.fnmatch_lines(["*usage: *", "*error: my_usage_error"])
["usage: pytest.py *", "pytest.py: error: my_usage_error"]
def test_remote_sys_path(pytester: pytest.Pytester) -> None:
"""Work around sys.path differences due to execnet using `python -c`."""
pytester.makepyfile(
"""
import sys
def test_sys_path():
assert "" not in sys.path
"""
) )
result = pytester.runpytest("-n1")
assert result.ret == 0

View File

@@ -1,273 +0,0 @@
from __future__ import print_function
import py
import pytest
import textwrap
import execnet
from _pytest.pytester import HookRecorder
from xdist import workermanage, newhooks
from xdist.workermanage import HostRSync, NodeManager
pytest_plugins = "pytester"
@pytest.fixture
def hookrecorder(request, config):
hookrecorder = HookRecorder(config.pluginmanager)
if hasattr(hookrecorder, "start_recording"):
hookrecorder.start_recording(newhooks)
request.addfinalizer(hookrecorder.finish_recording)
return hookrecorder
@pytest.fixture
def config(testdir):
return testdir.parseconfig()
@pytest.fixture
def mysetup(tmpdir):
class mysetup:
source = tmpdir.mkdir("source")
dest = tmpdir.mkdir("dest")
return mysetup()
@pytest.fixture
def workercontroller(monkeypatch):
class MockController(object):
def __init__(self, *args):
pass
def setup(self):
pass
monkeypatch.setattr(workermanage, "WorkerController", MockController)
return MockController
class TestNodeManagerPopen:
def test_popen_no_default_chdir(self, config):
gm = NodeManager(config, ["popen"])
assert gm.specs[0].chdir is None
def test_default_chdir(self, config):
specs = ["ssh=noco", "socket=xyz"]
for spec in NodeManager(config, specs).specs:
assert spec.chdir == "pyexecnetcache"
for spec in NodeManager(config, specs, defaultchdir="abc").specs:
assert spec.chdir == "abc"
def test_popen_makegateway_events(self, config, hookrecorder, workercontroller):
hm = NodeManager(config, ["popen"] * 2)
hm.setup_nodes(None)
call = hookrecorder.popcall("pytest_xdist_setupnodes")
assert len(call.specs) == 2
call = hookrecorder.popcall("pytest_xdist_newgateway")
assert call.gateway.spec == execnet.XSpec("popen")
assert call.gateway.id == "gw0"
call = hookrecorder.popcall("pytest_xdist_newgateway")
assert call.gateway.id == "gw1"
assert len(hm.group) == 2
hm.teardown_nodes()
assert not len(hm.group)
def test_popens_rsync(self, config, mysetup, workercontroller):
source = mysetup.source
hm = NodeManager(config, ["popen"] * 2)
hm.setup_nodes(None)
assert len(hm.group) == 2
for gw in hm.group:
class pseudoexec:
args = []
def __init__(self, *args):
self.args.extend(args)
def waitclose(self):
pass
gw.remote_exec = pseudoexec
notifications = []
for gw in hm.group:
hm.rsync(gw, source, notify=lambda *args: notifications.append(args))
assert not notifications
hm.teardown_nodes()
assert not len(hm.group)
assert "sys.path.insert" in gw.remote_exec.args[0]
def test_rsync_popen_with_path(self, config, mysetup, workercontroller):
source, dest = mysetup.source, mysetup.dest
hm = NodeManager(config, ["popen//chdir=%s" % dest] * 1)
hm.setup_nodes(None)
source.ensure("dir1", "dir2", "hello")
notifications = []
for gw in hm.group:
hm.rsync(gw, source, notify=lambda *args: notifications.append(args))
assert len(notifications) == 1
assert notifications[0] == ("rsyncrootready", hm.group["gw0"].spec, source)
hm.teardown_nodes()
dest = dest.join(source.basename)
assert dest.join("dir1").check()
assert dest.join("dir1", "dir2").check()
assert dest.join("dir1", "dir2", "hello").check()
def test_rsync_same_popen_twice(
self, config, mysetup, hookrecorder, workercontroller
):
source, dest = mysetup.source, mysetup.dest
hm = NodeManager(config, ["popen//chdir=%s" % dest] * 2)
hm.roots = []
hm.setup_nodes(None)
source.ensure("dir1", "dir2", "hello")
gw = hm.group[0]
hm.rsync(gw, source)
call = hookrecorder.popcall("pytest_xdist_rsyncstart")
assert call.source == source
assert len(call.gateways) == 1
assert call.gateways[0] in hm.group
call = hookrecorder.popcall("pytest_xdist_rsyncfinish")
class TestHRSync:
def test_hrsync_filter(self, mysetup):
source, _ = mysetup.source, mysetup.dest # noqa
source.ensure("dir", "file.txt")
source.ensure(".svn", "entries")
source.ensure(".somedotfile", "moreentries")
source.ensure("somedir", "editfile~")
syncer = HostRSync(source, ignores=NodeManager.DEFAULT_IGNORES)
files = list(source.visit(rec=syncer.filter, fil=syncer.filter))
assert len(files) == 3
basenames = [x.basename for x in files]
assert "dir" in basenames
assert "file.txt" in basenames
assert "somedir" in basenames
def test_hrsync_one_host(self, mysetup):
source, dest = mysetup.source, mysetup.dest
gw = execnet.makegateway("popen//chdir=%s" % dest)
finished = []
rsync = HostRSync(source)
rsync.add_target_host(gw, finished=lambda: finished.append(1))
source.join("hello.py").write("world")
rsync.send()
gw.exit()
assert dest.join(source.basename, "hello.py").check()
assert len(finished) == 1
class TestNodeManager:
@py.test.mark.xfail(run=False)
def test_rsync_roots_no_roots(self, testdir, mysetup):
mysetup.source.ensure("dir1", "file1").write("hello")
config = testdir.parseconfig(mysetup.source)
nodemanager = NodeManager(config, ["popen//chdir=%s" % mysetup.dest])
# assert nodemanager.config.topdir == source == config.topdir
nodemanager.makegateways()
nodemanager.rsync_roots()
p, = nodemanager.gwmanager.multi_exec(
"import os ; channel.send(os.getcwd())"
).receive_each()
p = py.path.local(p)
print("remote curdir", p)
assert p == mysetup.dest.join(config.topdir.basename)
assert p.join("dir1").check()
assert p.join("dir1", "file1").check()
def test_popen_rsync_subdir(self, testdir, mysetup, workercontroller):
source, dest = mysetup.source, mysetup.dest
dir1 = mysetup.source.mkdir("dir1")
dir2 = dir1.mkdir("dir2")
dir2.ensure("hello")
for rsyncroot in (dir1, source):
dest.remove()
nodemanager = NodeManager(
testdir.parseconfig(
"--tx", "popen//chdir=%s" % dest, "--rsyncdir", rsyncroot, source
)
)
nodemanager.setup_nodes(None) # calls .rsync_roots()
if rsyncroot == source:
dest = dest.join("source")
assert dest.join("dir1").check()
assert dest.join("dir1", "dir2").check()
assert dest.join("dir1", "dir2", "hello").check()
nodemanager.teardown_nodes()
def test_init_rsync_roots(self, testdir, mysetup, workercontroller):
source, dest = mysetup.source, mysetup.dest
dir2 = source.ensure("dir1", "dir2", dir=1)
source.ensure("dir1", "somefile", dir=1)
dir2.ensure("hello")
source.ensure("bogusdir", "file")
source.join("tox.ini").write(
textwrap.dedent(
"""
[pytest]
rsyncdirs=dir1/dir2
"""
)
)
config = testdir.parseconfig(source)
nodemanager = NodeManager(config, ["popen//chdir=%s" % dest])
nodemanager.setup_nodes(None) # calls .rsync_roots()
assert dest.join("dir2").check()
assert not dest.join("dir1").check()
assert not dest.join("bogus").check()
def test_rsyncignore(self, testdir, mysetup, workercontroller):
source, dest = mysetup.source, mysetup.dest
dir2 = source.ensure("dir1", "dir2", dir=1)
source.ensure("dir5", "dir6", "bogus")
source.ensure("dir5", "file")
dir2.ensure("hello")
source.ensure("foo", "bar")
source.ensure("bar", "foo")
source.join("tox.ini").write(
textwrap.dedent(
"""
[pytest]
rsyncdirs = dir1 dir5
rsyncignore = dir1/dir2 dir5/dir6 foo*
"""
)
)
config = testdir.parseconfig(source)
config.option.rsyncignore = ["bar"]
nodemanager = NodeManager(config, ["popen//chdir=%s" % dest])
nodemanager.setup_nodes(None) # calls .rsync_roots()
assert dest.join("dir1").check()
assert not dest.join("dir1", "dir2").check()
assert dest.join("dir5", "file").check()
assert not dest.join("dir6").check()
assert not dest.join("foo").check()
assert not dest.join("bar").check()
def test_optimise_popen(self, testdir, mysetup, workercontroller):
source = mysetup.source
specs = ["popen"] * 3
source.join("conftest.py").write("rsyncdirs = ['a']")
source.ensure("a", dir=1)
config = testdir.parseconfig(source)
nodemanager = NodeManager(config, specs)
nodemanager.setup_nodes(None) # calls .rysnc_roots()
for gwspec in nodemanager.specs:
assert gwspec._samefilesystem()
assert not gwspec.chdir
def test_ssh_setup_nodes(self, specssh, testdir):
testdir.makepyfile(
__init__="",
test_x="""
def test_one():
pass
""",
)
reprec = testdir.inline_run(
"-d", "--rsyncdir=%s" % testdir.tmpdir, "--tx", specssh, testdir.tmpdir
)
rep, = reprec.getreports("pytest_runtest_logreport")
assert rep.passed

View File

@@ -0,0 +1,405 @@
import execnet
import pytest
import shutil
import textwrap
import warnings
from pathlib import Path
from util import generate_warning
from xdist import workermanage
from xdist._path import visit_path
from xdist.remote import serialize_warning_message
from xdist.workermanage import HostRSync, NodeManager, unserialize_warning_message
pytest_plugins = "pytester"
@pytest.fixture
def hookrecorder(request, config, pytester: pytest.Pytester):
hookrecorder = pytester.make_hook_recorder(config.pluginmanager)
return hookrecorder
@pytest.fixture
def config(pytester: pytest.Pytester):
return pytester.parseconfig()
@pytest.fixture
def source(tmp_path: Path) -> Path:
source = tmp_path / "source"
source.mkdir()
return source
@pytest.fixture
def dest(tmp_path: Path) -> Path:
dest = tmp_path / "dest"
dest.mkdir()
return dest
@pytest.fixture
def workercontroller(monkeypatch: pytest.MonkeyPatch):
class MockController:
def __init__(self, *args):
pass
def setup(self):
pass
monkeypatch.setattr(workermanage, "WorkerController", MockController)
return MockController
class TestNodeManagerPopen:
def test_popen_no_default_chdir(self, config) -> None:
gm = NodeManager(config, ["popen"])
assert gm.specs[0].chdir is None
def test_default_chdir(self, config) -> None:
specs = ["ssh=noco", "socket=xyz"]
for spec in NodeManager(config, specs).specs:
assert spec.chdir == "pyexecnetcache"
for spec in NodeManager(config, specs, defaultchdir="abc").specs:
assert spec.chdir == "abc"
def test_popen_makegateway_events(
self, config, hookrecorder, workercontroller
) -> None:
hm = NodeManager(config, ["popen"] * 2)
hm.setup_nodes(None)
call = hookrecorder.popcall("pytest_xdist_setupnodes")
assert len(call.specs) == 2
call = hookrecorder.popcall("pytest_xdist_newgateway")
assert call.gateway.spec == execnet.XSpec("popen")
assert call.gateway.id == "gw0"
call = hookrecorder.popcall("pytest_xdist_newgateway")
assert call.gateway.id == "gw1"
assert len(hm.group) == 2
hm.teardown_nodes()
assert not len(hm.group)
def test_popens_rsync(
self, config, source: Path, dest: Path, workercontroller
) -> None:
hm = NodeManager(config, ["popen"] * 2)
hm.setup_nodes(None)
assert len(hm.group) == 2
for gw in hm.group:
class pseudoexec:
args = [] # type: ignore[var-annotated]
def __init__(self, *args):
self.args.extend(args)
def waitclose(self):
pass
gw.remote_exec = pseudoexec
notifications = []
for gw in hm.group:
hm.rsync(gw, source, notify=lambda *args: notifications.append(args))
assert not notifications
hm.teardown_nodes()
assert not len(hm.group)
assert "sys.path.insert" in gw.remote_exec.args[0]
def test_rsync_popen_with_path(
self, config, source: Path, dest: Path, workercontroller
) -> None:
hm = NodeManager(config, ["popen//chdir=%s" % dest] * 1)
hm.setup_nodes(None)
source.joinpath("dir1", "dir2").mkdir(parents=True)
source.joinpath("dir1", "dir2", "hello").touch()
notifications = []
for gw in hm.group:
hm.rsync(gw, source, notify=lambda *args: notifications.append(args))
assert len(notifications) == 1
assert notifications[0] == ("rsyncrootready", hm.group["gw0"].spec, source)
hm.teardown_nodes()
dest = dest.joinpath(source.name)
assert dest.joinpath("dir1").exists()
assert dest.joinpath("dir1", "dir2").exists()
assert dest.joinpath("dir1", "dir2", "hello").exists()
def test_rsync_same_popen_twice(
self,
config,
source: Path,
dest: Path,
hookrecorder,
workercontroller,
) -> None:
hm = NodeManager(config, ["popen//chdir=%s" % dest] * 2)
hm.roots = []
hm.setup_nodes(None)
source.joinpath("dir1", "dir2").mkdir(parents=True)
source.joinpath("dir1", "dir2", "hello").touch()
gw = hm.group[0]
hm.rsync(gw, source)
call = hookrecorder.popcall("pytest_xdist_rsyncstart")
assert call.source == source
assert len(call.gateways) == 1
assert call.gateways[0] in hm.group
call = hookrecorder.popcall("pytest_xdist_rsyncfinish")
class TestHRSync:
def test_hrsync_filter(self, source: Path, dest: Path) -> None:
source.joinpath("dir").mkdir()
source.joinpath("dir", "file.txt").touch()
source.joinpath(".svn").mkdir()
source.joinpath(".svn", "entries").touch()
source.joinpath(".somedotfile").mkdir()
source.joinpath(".somedotfile", "moreentries").touch()
source.joinpath("somedir").mkdir()
source.joinpath("somedir", "editfile~").touch()
syncer = HostRSync(source, ignores=NodeManager.DEFAULT_IGNORES)
files = list(visit_path(source, recurse=syncer.filter, filter=syncer.filter))
names = {x.name for x in files}
assert names == {"dir", "file.txt", "somedir"}
def test_hrsync_one_host(self, source: Path, dest: Path) -> None:
gw = execnet.makegateway("popen//chdir=%s" % dest)
finished = []
rsync = HostRSync(source)
rsync.add_target_host(gw, finished=lambda: finished.append(1))
source.joinpath("hello.py").write_text("world")
rsync.send()
gw.exit()
assert dest.joinpath(source.name, "hello.py").exists()
assert len(finished) == 1
class TestNodeManager:
@pytest.mark.xfail(run=False)
def test_rsync_roots_no_roots(
self, pytester: pytest.Pytester, source: Path, dest: Path
) -> None:
source.joinpath("dir1").mkdir()
source.joinpath("dir1", "file1").write_text("hello")
config = pytester.parseconfig(source)
nodemanager = NodeManager(config, ["popen//chdir=%s" % dest])
# assert nodemanager.config.topdir == source == config.topdir
nodemanager.makegateways() # type: ignore[attr-defined]
nodemanager.rsync_roots() # type: ignore[call-arg]
(p,) = nodemanager.gwmanager.multi_exec( # type: ignore[attr-defined]
"import os ; channel.send(os.getcwd())"
).receive_each()
p = Path(p)
print("remote curdir", p)
assert p == dest.joinpath(config.rootpath.name)
assert p.joinpath("dir1").check()
assert p.joinpath("dir1", "file1").check()
def test_popen_rsync_subdir(
self, pytester: pytest.Pytester, source: Path, dest: Path, workercontroller
) -> None:
dir1 = source / "dir1"
dir1.mkdir()
dir2 = dir1 / "dir2"
dir2.mkdir()
dir2.joinpath("hello").touch()
for rsyncroot in (dir1, source):
shutil.rmtree(str(dest), ignore_errors=True)
nodemanager = NodeManager(
pytester.parseconfig(
"--tx", "popen//chdir=%s" % dest, "--rsyncdir", rsyncroot, source
)
)
nodemanager.setup_nodes(None) # calls .rsync_roots()
if rsyncroot == source:
dest = dest.joinpath("source")
assert dest.joinpath("dir1").exists()
assert dest.joinpath("dir1", "dir2").exists()
assert dest.joinpath("dir1", "dir2", "hello").exists()
nodemanager.teardown_nodes()
@pytest.mark.parametrize(
"flag, expects_report", [("-q", False), ("", False), ("-v", True)]
)
def test_rsync_report(
self,
pytester: pytest.Pytester,
source: Path,
dest: Path,
workercontroller,
capsys: pytest.CaptureFixture[str],
flag: str,
expects_report: bool,
) -> None:
dir1 = source / "dir1"
dir1.mkdir()
args = ["--tx", "popen//chdir=%s" % dest, "--rsyncdir", str(dir1), str(source)]
if flag:
args.append(flag)
nodemanager = NodeManager(pytester.parseconfig(*args))
nodemanager.setup_nodes(None) # calls .rsync_roots()
out, _ = capsys.readouterr()
if expects_report:
assert "<= pytest/__init__.py" in out
else:
assert "<= pytest/__init__.py" not in out
def test_init_rsync_roots(
self, pytester: pytest.Pytester, source: Path, dest: Path, workercontroller
) -> None:
dir2 = source.joinpath("dir1", "dir2")
dir2.mkdir(parents=True)
source.joinpath("dir1", "somefile").mkdir()
dir2.joinpath("hello").touch()
source.joinpath("bogusdir").mkdir()
source.joinpath("bogusdir", "file").touch()
source.joinpath("tox.ini").write_text(
textwrap.dedent(
"""
[pytest]
rsyncdirs=dir1/dir2
"""
)
)
config = pytester.parseconfig(source)
nodemanager = NodeManager(config, ["popen//chdir=%s" % dest])
nodemanager.setup_nodes(None) # calls .rsync_roots()
assert dest.joinpath("dir2").exists()
assert not dest.joinpath("dir1").exists()
assert not dest.joinpath("bogus").exists()
def test_rsyncignore(
self, pytester: pytest.Pytester, source: Path, dest: Path, workercontroller
) -> None:
dir2 = source.joinpath("dir1", "dir2")
dir2.mkdir(parents=True)
source.joinpath("dir5", "dir6").mkdir(parents=True)
source.joinpath("dir5", "dir6", "bogus").touch()
source.joinpath("dir5", "file").touch()
dir2.joinpath("hello").touch()
source.joinpath("foo").mkdir()
source.joinpath("foo", "bar").touch()
source.joinpath("bar").mkdir()
source.joinpath("bar", "foo").touch()
source.joinpath("tox.ini").write_text(
textwrap.dedent(
"""
[pytest]
rsyncdirs = dir1 dir5
rsyncignore = dir1/dir2 dir5/dir6 foo*
"""
)
)
config = pytester.parseconfig(source)
config.option.rsyncignore = ["bar"]
nodemanager = NodeManager(config, ["popen//chdir=%s" % dest])
nodemanager.setup_nodes(None) # calls .rsync_roots()
assert dest.joinpath("dir1").exists()
assert not dest.joinpath("dir1", "dir2").exists()
assert dest.joinpath("dir5", "file").exists()
assert not dest.joinpath("dir6").exists()
assert not dest.joinpath("foo").exists()
assert not dest.joinpath("bar").exists()
def test_optimise_popen(
self, pytester: pytest.Pytester, source: Path, dest: Path, workercontroller
) -> None:
specs = ["popen"] * 3
source.joinpath("conftest.py").write_text("rsyncdirs = ['a']")
source.joinpath("a").mkdir()
config = pytester.parseconfig(source)
nodemanager = NodeManager(config, specs)
nodemanager.setup_nodes(None) # calls .rysnc_roots()
for gwspec in nodemanager.specs:
assert gwspec._samefilesystem()
assert not gwspec.chdir
def test_ssh_setup_nodes(self, specssh: str, pytester: pytest.Pytester) -> None:
pytester.makepyfile(
__init__="",
test_x="""
def test_one():
pass
""",
)
reprec = pytester.inline_run(
"-d", "--rsyncdir=%s" % pytester.path, "--tx", specssh, pytester.path
)
(rep,) = reprec.getreports("pytest_runtest_logreport")
assert rep.passed
class MyWarning(UserWarning):
pass
@pytest.mark.parametrize(
"w_cls",
[
UserWarning,
MyWarning,
"Imported",
pytest.param(
"Nested",
marks=pytest.mark.xfail(reason="Nested warning classes are not supported."),
),
],
)
def test_unserialize_warning_msg(w_cls):
"""Test that warning serialization process works well"""
# Create a test warning message
with pytest.warns(UserWarning) as w:
if not isinstance(w_cls, str):
warnings.warn("hello", w_cls)
elif w_cls == "Imported":
generate_warning()
elif w_cls == "Nested":
# dynamic creation
class MyWarning2(UserWarning):
pass
warnings.warn("hello", MyWarning2)
# Unpack
assert len(w) == 1
w_msg = w[0]
# Serialize and deserialize
data = serialize_warning_message(w_msg)
w_msg2 = unserialize_warning_message(data)
# Compare the two objects
all_keys = set(vars(w_msg).keys()).union(set(vars(w_msg2).keys()))
for k in all_keys:
v1 = getattr(w_msg, k)
v2 = getattr(w_msg2, k)
if k == "message":
assert type(v1) == type(v2)
assert v1.args == v2.args
else:
assert v1 == v2
class MyWarningUnknown(UserWarning):
# Changing the __module__ attribute is only safe if class can be imported
# from there
__module__ = "unknown"
def test_warning_serialization_tweaked_module():
"""Test for GH#404"""
# Create a test warning message
with pytest.warns(UserWarning) as w:
warnings.warn("hello", MyWarningUnknown)
# Unpack
assert len(w) == 1
w_msg = w[0]
# Serialize and deserialize
data = serialize_warning_message(w_msg)
# __module__ cannot be found!
with pytest.raises(ModuleNotFoundError):
unserialize_warning_message(data)

9
testing/util.py Normal file
View File

@@ -0,0 +1,9 @@
import warnings
class MyWarning2(UserWarning):
pass
def generate_warning():
warnings.warn(MyWarning2("hello"))

53
tox.ini
View File

@@ -1,41 +1,68 @@
[tox] [tox]
envlist= envlist=
linting linting
py{27,34,35,36,37}-pytestlatest py{37,38,39,310,311}-pytestlatest
py{27,36}-pytest{master,features} py310-pytestmain
py310-psutil
py310-setproctitle
isolated_build = true
[testenv] [testenv]
changedir=testing extras = testing
passenv = USER USERNAME
deps = deps =
pytestlatest: pytest pytestlatest: pytest
pytestmaster: git+https://github.com/pytest-dev/pytest.git@master pytestmain: git+https://github.com/pytest-dev/pytest.git
pytestfeatures: git+https://github.com/pytest-dev/pytest.git@features commands=
filelock pytest {posargs}
[testenv:py310-psutil]
extras =
testing
psutil
commands =
pytest {posargs:-k psutil}
[testenv:py310-setproctitle]
extras =
testing
setproctitle
deps = pytest
commands = commands =
pytest {posargs} pytest {posargs}
[testenv:linting] [testenv:linting]
skipsdist = True skip_install = True
usedevelop = True usedevelop = True
passenv = PRE_COMMIT_HOME
deps = deps =
pre-commit pre-commit
commands = pre-commit run --all-files --show-diff-on-failure commands = pre-commit run --all-files --show-diff-on-failure
[testenv:release] [testenv:release]
changedir= changedir=
decription = do a release, required posarg of the version number description = do a release, required posarg of the version number
basepython = python3.6 basepython = python3.10
skipsdist = True skipsdist = True
usedevelop = True usedevelop = True
passenv = * passenv = *
deps = deps =
towncrier towncrier
commands = commands =
towncrier --version {posargs} --yes towncrier build --version {posargs} --yes
[testenv:docs]
basepython = python3.10
usedevelop = True
deps =
sphinx
sphinx_rtd_theme
commands =
sphinx-build -W --keep-going -b html docs docs/_build/html {posargs:}
[pytest] [pytest]
addopts = -ra # pytest-services also defines a worker_id fixture, disable
# it so they don't conflict with each other (#611).
addopts = -ra -p no:pytest-services
testpaths = testing
[flake8] [flake8]
max-line-length = 120 max-line-length = 120

View File

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

View File

@@ -1,52 +0,0 @@
"""
xdist hooks.
Additionally, pytest-xdist will also decorate a few other hooks
with the worker instance that executed the hook originally:
``pytest_runtest_logreport``: ``rep`` parameter has a ``node`` attribute.
You can use this hooks just as you would use normal pytest hooks, but some care
must be taken in plugins in case ``xdist`` is not installed. Please see:
http://pytest.org/en/latest/writing_plugins.html#optionally-using-hooks-from-3rd-party-plugins
"""
import pytest
def pytest_xdist_setupnodes(config, specs):
""" called before any remote node is set up. """
def pytest_xdist_newgateway(gateway):
""" called on new raw gateway creation. """
def pytest_xdist_rsyncstart(source, gateways):
""" called before rsyncing a directory to remote gateways takes place. """
def pytest_xdist_rsyncfinish(source, gateways):
""" called after rsyncing a directory to remote gateways takes place. """
def pytest_configure_node(node):
""" configure node information before it gets instantiated. """
def pytest_testnodeready(node):
""" Test Node is ready to operate. """
def pytest_testnodedown(node, error):
""" Test Node is down. """
def pytest_xdist_node_collection_finished(node, ids):
"""called by the master node when a node finishes collecting.
"""
@pytest.mark.firstresult
def pytest_xdist_make_scheduler(config, log):
""" return a node scheduler implementation """

View File

@@ -1,212 +0,0 @@
import os
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
def parse_numprocesses(s):
if s == "auto":
return auto_detect_cpus()
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(
"--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",
)
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"
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"):
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"