Compare commits

..

967 Commits

Author SHA1 Message Date
pre-commit-ci[bot]
239def9be9 Merge pull request #1227 from pytest-dev/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2025-07-14 19:34:51 -03:00
pre-commit-ci[bot]
ec87d0a139 [pre-commit.ci] pre-commit autoupdate (#1226)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.12.1 → v0.12.2](https://github.com/astral-sh/ruff-pre-commit/compare/v0.12.1...v0.12.2)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-07-08 08:43:31 +02:00
Bruno Oliveira
fe10f1c411 Merge pull request #1224 from pytest-dev/release-3.8.0
Release 3.8.0
2025-07-01 10:31:26 -03:00
pre-commit-ci[bot]
3572d0b13c [pre-commit.ci] pre-commit autoupdate (#1223)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.12.0 → v0.12.1](https://github.com/astral-sh/ruff-pre-commit/compare/v0.12.0...v0.12.1)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-06-30 18:33:44 -03:00
Bruno Oliveira
1e3e4dc165 Release 3.8.0 2025-06-30 11:05:48 -03:00
Shane Smiskol
600aad575a Ensure all xdist group names are strings (#1216) 2025-06-30 11:01:27 -03:00
darwintree
9d7ba5b5fb Add --no-loadscope-reorder and --loadscope-reorder options (#1217)
Based on #1098.

Closes #1098

---------

Co-authored-by: Toan Vuong <toan.vuong@hyperscience.com>
2025-06-30 10:59:40 -03:00
Bruno Oliveira
532f07fb18 Merge pull request #1210 from pytest-dev/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2025-06-24 08:24:11 -03:00
Bruno Oliveira
0883ad085e Fix Path usage in test_rsync_roots_no_roots 2025-06-24 08:21:48 -03:00
pre-commit-ci[bot]
58a51bc140 [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.11.11 → v0.12.0](https://github.com/astral-sh/ruff-pre-commit/compare/v0.11.11...v0.12.0)
- [github.com/pre-commit/mirrors-mypy: v1.15.0 → v1.16.1](https://github.com/pre-commit/mirrors-mypy/compare/v1.15.0...v1.16.1)
2025-06-23 20:16:47 +00:00
Bruno Oliveira
59a2ad0150 Merge pull request #1220 from pytest-dev/dependabot/github_actions/github-actions-33a29a28f8
build(deps): bump hynek/build-and-inspect-python-package from 2.12 to 2.13 in the github-actions group
2025-06-16 17:43:36 -03:00
dependabot[bot]
d42b9c7204 build(deps): bump hynek/build-and-inspect-python-package
Bumps the github-actions group with 1 update: [hynek/build-and-inspect-python-package](https://github.com/hynek/build-and-inspect-python-package).


Updates `hynek/build-and-inspect-python-package` from 2.12 to 2.13
- [Release notes](https://github.com/hynek/build-and-inspect-python-package/releases)
- [Changelog](https://github.com/hynek/build-and-inspect-python-package/blob/main/CHANGELOG.md)
- [Commits](https://github.com/hynek/build-and-inspect-python-package/compare/v2.12...v2.13)

---
updated-dependencies:
- dependency-name: hynek/build-and-inspect-python-package
  dependency-version: '2.13'
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-16 19:13:50 +00:00
Bruno Oliveira
ebfcb99072 Merge pull request #1206 from pytest-dev/release-3.7.0
Release 3.7.0
2025-05-26 18:24:03 -03:00
pre-commit-ci[bot]
23b7fd6054 [pre-commit.ci] pre-commit autoupdate (#1207)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.11.10 → v0.11.11](https://github.com/astral-sh/ruff-pre-commit/compare/v0.11.10...v0.11.11)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-05-26 22:52:00 +02:00
Bruno Oliveira
b561916eaf Release 3.7.0 2025-05-26 14:51:07 -03:00
pre-commit-ci[bot]
71bd3ede19 [pre-commit.ci] pre-commit autoupdate (#1204)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.11.9 → v0.11.10](https://github.com/astral-sh/ruff-pre-commit/compare/v0.11.9...v0.11.10)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-05-19 22:37:54 +02:00
Ygor Pontelo
65de0ab30e Join multiple xdist_group markers (#1201)
Fixes #1200

---------

Co-authored-by: Bruno Oliveira <bruno@soliv.dev>
2025-05-14 21:51:59 -03:00
pre-commit-ci[bot]
199f949716 [pre-commit.ci] pre-commit autoupdate (#1202)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.11.8 → v0.11.9](https://github.com/astral-sh/ruff-pre-commit/compare/v0.11.8...v0.11.9)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-05-12 22:33:00 +02:00
pre-commit-ci[bot]
b0215da1ed [pre-commit.ci] pre-commit autoupdate (#1199)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.11.7 → v0.11.8](https://github.com/astral-sh/ruff-pre-commit/compare/v0.11.7...v0.11.8)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-05-05 22:53:19 +02:00
pre-commit-ci[bot]
18e47853ed [pre-commit.ci] pre-commit autoupdate (#1198)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.11.6 → v0.11.7](https://github.com/astral-sh/ruff-pre-commit/compare/v0.11.6...v0.11.7)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-04-28 22:58:57 +02:00
pre-commit-ci[bot]
731c05d86a [pre-commit.ci] pre-commit autoupdate (#1197)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.11.5 → v0.11.6](https://github.com/astral-sh/ruff-pre-commit/compare/v0.11.5...v0.11.6)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-04-21 22:34:41 +02:00
pre-commit-ci[bot]
16c5be3203 [pre-commit.ci] pre-commit autoupdate (#1195)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.11.4 → v0.11.5](https://github.com/astral-sh/ruff-pre-commit/compare/v0.11.4...v0.11.5)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-04-15 04:51:22 +02:00
pre-commit-ci[bot]
e03757a1d7 [pre-commit.ci] pre-commit autoupdate (#1193)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.11.2 → v0.11.4](https://github.com/astral-sh/ruff-pre-commit/compare/v0.11.2...v0.11.4)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-04-08 06:16:53 +02:00
Ronny Pfannschmidt
f1d81a4402 Merge pull request #1192 from adamchainz/document_disabling
Document disabling with '-n 0'
2025-04-02 20:07:48 +02:00
Adam Johnson
8074c633c8 Document disabling with '-n 0' 2025-04-02 11:36:41 +01:00
Marc Mueller
f83299cd64 Use SPDX license identifier (#1190)
Co-authored-by: Pierre Sassoulas <pierre.sassoulas@gmail.com>
2025-03-25 13:36:07 +01:00
pre-commit-ci[bot]
4d970380ee [pre-commit.ci] pre-commit autoupdate (#1191)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.11.0 → v0.11.2](https://github.com/astral-sh/ruff-pre-commit/compare/v0.11.0...v0.11.2)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-03-24 21:58:22 +01:00
pre-commit-ci[bot]
10941bad70 [pre-commit.ci] pre-commit autoupdate (#1188)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.9.10 → v0.11.0](https://github.com/astral-sh/ruff-pre-commit/compare/v0.9.10...v0.11.0)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-03-17 22:43:23 +01:00
pre-commit-ci[bot]
57e19eca18 [pre-commit.ci] pre-commit autoupdate (#1187)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.9.9 → v0.9.10](https://github.com/astral-sh/ruff-pre-commit/compare/v0.9.9...v0.9.10)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-03-10 22:18:38 +01:00
pre-commit-ci[bot]
3dab30ec8a [pre-commit.ci] pre-commit autoupdate (#1185)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.9.7 → v0.9.9](https://github.com/astral-sh/ruff-pre-commit/compare/v0.9.7...v0.9.9)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-03-03 22:37:10 +01:00
pre-commit-ci[bot]
3508f8c8ad [pre-commit.ci] pre-commit autoupdate (#1181)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.9.6 → v0.9.7](https://github.com/astral-sh/ruff-pre-commit/compare/v0.9.6...v0.9.7)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-02-26 08:04:12 -03:00
pre-commit-ci[bot]
84a854438c [pre-commit.ci] pre-commit autoupdate (#1177)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.9.4 → v0.9.6](https://github.com/astral-sh/ruff-pre-commit/compare/v0.9.4...v0.9.6)
- [github.com/pre-commit/mirrors-mypy: v1.14.1 → v1.15.0](https://github.com/pre-commit/mirrors-mypy/compare/v1.14.1...v1.15.0)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-02-11 01:37:37 +01:00
pre-commit-ci[bot]
cb97e7c31a [pre-commit.ci] pre-commit autoupdate (#1175)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.9.2 → v0.9.4](https://github.com/astral-sh/ruff-pre-commit/compare/v0.9.2...v0.9.4)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-02-04 08:53:44 +01:00
dependabot[bot]
adfba2bbb8 build(deps): bump the github-actions group with 2 updates (#1174)
Bumps the github-actions group with 2 updates: [hynek/build-and-inspect-python-package](https://github.com/hynek/build-and-inspect-python-package) and [pypa/gh-action-pypi-publish](https://github.com/pypa/gh-action-pypi-publish).


Updates `hynek/build-and-inspect-python-package` from 2.11 to 2.12
- [Release notes](https://github.com/hynek/build-and-inspect-python-package/releases)
- [Changelog](https://github.com/hynek/build-and-inspect-python-package/blob/main/CHANGELOG.md)
- [Commits](https://github.com/hynek/build-and-inspect-python-package/compare/v2.11...v2.12)

Updates `pypa/gh-action-pypi-publish` from 1.12.3 to 1.12.4
- [Release notes](https://github.com/pypa/gh-action-pypi-publish/releases)
- [Commits](https://github.com/pypa/gh-action-pypi-publish/compare/v1.12.3...v1.12.4)

---
updated-dependencies:
- dependency-name: hynek/build-and-inspect-python-package
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
- dependency-name: pypa/gh-action-pypi-publish
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-27 20:25:10 -03:00
pre-commit-ci[bot]
ab798d52a5 [pre-commit.ci] pre-commit autoupdate (#1173)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.9.1 → v0.9.2](https://github.com/astral-sh/ruff-pre-commit/compare/v0.9.1...v0.9.2)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-01-21 05:03:12 +01:00
Ronny Pfannschmidt
987c2ca896 Merge pull request #1171 from Alon-L/feature/proxy-gateways
Add proxy gateways
2025-01-18 13:14:31 +01:00
Ronny Pfannschmidt
8346acc7fb Merge branch 'master' into feature/proxy-gateways 2025-01-18 10:48:48 +01:00
pre-commit-ci[bot]
59f743487f [pre-commit.ci] pre-commit autoupdate (#1172)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.8.6 → v0.9.1](https://github.com/astral-sh/ruff-pre-commit/compare/v0.8.6...v0.9.1)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-01-14 09:57:14 +01:00
pre-commit-ci[bot]
c11ca7a8fe [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2025-01-12 19:32:01 +00:00
Alon Livne
8632b38203 add tests for proxy gateways
The test `test_proxy_gateway_setup_nodes` makes sure that 2 execnet
gateways are created, but only one node is allocated.

The test `test_proxy_gateway` tries to run a test on a proxy.
2025-01-12 21:30:43 +02:00
pre-commit-ci[bot]
35db0e79c2 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2025-01-12 18:38:19 +00:00
Alon Livne
333e2d3435 add proxy gateways changelog 2025-01-12 20:37:52 +02:00
Alon Livne
17ac3359e3 add proxy gateway docs
Add docs for using proxy gateways to run multiple workers on remote
machines
2025-01-12 20:36:02 +02:00
Alon Livne
a9a303f688 add proxy gateways with --px arg
Proxy gateways do not run workers, and are meant to be passed with the
`via` attribute to additional gateways.
They are useful for running multiple workers on remote machines.

Example usage:
```
pytest -sv --dist=load --px "id=my_proxy//socket=IP:PORT" --tx "5*popen//via=my_proxy"
```

Proxy gateways do not run workers, anda re meant to be passed
2025-01-12 20:33:31 +02:00
pre-commit-ci[bot]
a82981f444 [pre-commit.ci] pre-commit autoupdate (#1168)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.8.4 → v0.8.6](https://github.com/astral-sh/ruff-pre-commit/compare/v0.8.4...v0.8.6)
- [github.com/pre-commit/mirrors-mypy: v1.14.0 → v1.14.1](https://github.com/pre-commit/mirrors-mypy/compare/v1.14.0...v1.14.1)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-01-07 06:23:53 +01:00
pre-commit-ci[bot]
8c6c658f3a [pre-commit.ci] pre-commit autoupdate (#1167)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.8.3 → v0.8.4](https://github.com/astral-sh/ruff-pre-commit/compare/v0.8.3...v0.8.4)
- [github.com/pre-commit/mirrors-mypy: v1.13.0 → v1.14.0](https://github.com/pre-commit/mirrors-mypy/compare/v1.13.0...v1.14.0)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-12-24 07:19:37 +01:00
pre-commit-ci[bot]
003d4e0a30 [pre-commit.ci] pre-commit autoupdate (#1166)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.8.2 → v0.8.3](https://github.com/astral-sh/ruff-pre-commit/compare/v0.8.2...v0.8.3)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Pierre Sassoulas <pierre.sassoulas@gmail.com>
2024-12-17 09:09:20 -03:00
🇺🇦 Sviatoslav Sydorenko (Святослав Сидоренко)
42d3204015 Merge pull request #1165 from pytest-dev/dependabot/github_actions/github-actions-45e9fa06d3
build(deps): bump hynek/build-and-inspect-python-package from 2.10 to 2.11 in the github-actions group
2024-12-17 02:49:53 +01:00
dependabot[bot]
5c1de86638 build(deps): bump hynek/build-and-inspect-python-package
Bumps the github-actions group with 1 update: [hynek/build-and-inspect-python-package](https://github.com/hynek/build-and-inspect-python-package).


Updates `hynek/build-and-inspect-python-package` from 2.10 to 2.11
- [Release notes](https://github.com/hynek/build-and-inspect-python-package/releases)
- [Changelog](https://github.com/hynek/build-and-inspect-python-package/blob/main/CHANGELOG.md)
- [Commits](https://github.com/hynek/build-and-inspect-python-package/compare/v2.10...v2.11)

---
updated-dependencies:
- dependency-name: hynek/build-and-inspect-python-package
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-16 17:43:16 +00:00
pre-commit-ci[bot]
6eb403d612 [pre-commit.ci] pre-commit autoupdate (#1164)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.8.1 → v0.8.2](https://github.com/astral-sh/ruff-pre-commit/compare/v0.8.1...v0.8.2)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-12-10 07:32:14 +01:00
🇺🇦 Sviatoslav Sydorenko (Святослав Сидоренко)
c9dce96b37 Merge pull request #1163 from pytest-dev/dependabot/github_actions/github-actions-088cad1836
build(deps): bump pypa/gh-action-pypi-publish from 1.12.2 to 1.12.3 in the github-actions group
2024-12-09 20:25:38 +01:00
dependabot[bot]
bcf9ebda2d build(deps): bump pypa/gh-action-pypi-publish
Bumps the github-actions group with 1 update: [pypa/gh-action-pypi-publish](https://github.com/pypa/gh-action-pypi-publish).


Updates `pypa/gh-action-pypi-publish` from 1.12.2 to 1.12.3
- [Release notes](https://github.com/pypa/gh-action-pypi-publish/releases)
- [Commits](https://github.com/pypa/gh-action-pypi-publish/compare/v1.12.2...v1.12.3)

---
updated-dependencies:
- dependency-name: pypa/gh-action-pypi-publish
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-09 17:37:00 +00:00
Bruno Oliveira
39c62b30a4 Drop EOL Python 3.8 support (#1162)
* Drop EOL Python 3.8 support

* Run pyupgrade --py39-plus on all files

* Revert using direct annotations in looponfail.py

We need to use string annotations otherwise we get this error:

```
ValueError: ("the use of non-builtin globals isn't supported", ['execnet', 'Any'])
```
2024-12-07 10:08:45 -03:00
Bruno Oliveira
31bee7c567 Revert using direct annotations in looponfail.py
We need to use string annotations otherwise we get this error:

```
ValueError: ("the use of non-builtin globals isn't supported", ['execnet', 'Any'])
```
2024-12-07 08:57:08 -03:00
Bruno Oliveira
475b346fd9 Run pyupgrade --py39-plus on all files 2024-12-07 08:47:25 -03:00
Bruno Oliveira
6dc8c2a66d Drop EOL Python 3.8 support 2024-12-07 08:47:25 -03:00
pre-commit-ci[bot]
c3d7b7784a [pre-commit.ci] pre-commit autoupdate (#1160)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.8.0 → v0.8.1](https://github.com/astral-sh/ruff-pre-commit/compare/v0.8.0...v0.8.1)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-12-02 21:41:02 -03:00
pre-commit-ci[bot]
b4eb2cae8f [pre-commit.ci] pre-commit autoupdate (#1158)
* [pre-commit.ci] pre-commit autoupdate

updates:
- [github.com/astral-sh/ruff-pre-commit: v0.7.4 → v0.8.0](https://github.com/astral-sh/ruff-pre-commit/compare/v0.7.4...v0.8.0)

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-11-26 08:59:28 +01:00
pre-commit-ci[bot]
26e3b35f7e [pre-commit.ci] pre-commit autoupdate (#1156)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.7.3 → v0.7.4](https://github.com/astral-sh/ruff-pre-commit/compare/v0.7.3...v0.7.4)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-11-19 07:30:56 +01:00
Ronny Pfannschmidt
65042b05da Merge pull request #1153 from pytest-dev/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2024-11-12 06:57:46 +01:00
pre-commit-ci[bot]
aca8dd4a18 [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.7.2 → v0.7.3](https://github.com/astral-sh/ruff-pre-commit/compare/v0.7.2...v0.7.3)
2024-11-11 23:01:32 +00:00
Sviatoslav Sydorenko (Святослав Сидоренко)
7deba63fc8 Merge pull request #1152 from pytest-dev/dependabot/github_actions/github-actions-5da8da3d55 2024-11-11 19:27:53 +01:00
dependabot[bot]
a6d7ec19e2 build(deps): bump pypa/gh-action-pypi-publish
Bumps the github-actions group with 1 update: [pypa/gh-action-pypi-publish](https://github.com/pypa/gh-action-pypi-publish).


Updates `pypa/gh-action-pypi-publish` from 1.11.0 to 1.12.2
- [Release notes](https://github.com/pypa/gh-action-pypi-publish/releases)
- [Commits](https://github.com/pypa/gh-action-pypi-publish/compare/v1.11.0...v1.12.2)

---
updated-dependencies:
- dependency-name: pypa/gh-action-pypi-publish
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-11 17:39:50 +00:00
dependabot[bot]
0910472e2a build(deps): bump the github-actions group with 2 updates (#1149)
Bumps the github-actions group with 2 updates: [hynek/build-and-inspect-python-package](https://github.com/hynek/build-and-inspect-python-package) and [pypa/gh-action-pypi-publish](https://github.com/pypa/gh-action-pypi-publish).


Updates `hynek/build-and-inspect-python-package` from 2.9 to 2.10
- [Release notes](https://github.com/hynek/build-and-inspect-python-package/releases)
- [Changelog](https://github.com/hynek/build-and-inspect-python-package/blob/main/CHANGELOG.md)
- [Commits](https://github.com/hynek/build-and-inspect-python-package/compare/v2.9...v2.10)

Updates `pypa/gh-action-pypi-publish` from 1.10.3 to 1.11.0
- [Release notes](https://github.com/pypa/gh-action-pypi-publish/releases)
- [Commits](https://github.com/pypa/gh-action-pypi-publish/compare/v1.10.3...v1.11.0)

---
updated-dependencies:
- dependency-name: hynek/build-and-inspect-python-package
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
- dependency-name: pypa/gh-action-pypi-publish
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-05 20:55:10 -03:00
pre-commit-ci[bot]
ec3ce293ef [pre-commit.ci] pre-commit autoupdate (#1150)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.7.1 → v0.7.2](https://github.com/astral-sh/ruff-pre-commit/compare/v0.7.1...v0.7.2)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-11-05 09:34:35 +01:00
Aleksandr Mezin
9788f12e9e Make 'steal' command atomic (#1144)
Either unschedule all requested tests, or none if it's not possible -
if some of the requested tests have already been processed by the time
the request arrives. It may happen if the worker runs tests faster than
the controller receives and processes status updates. But in this case
maybe it's just better to let the worker keep running.

This is a prerequisite for group/scope support in worksteal scheduler -
so they won't be broken up incorrectly.

This change could break schedulers that use "steal" command. However:

1) worksteal scheduler doesn't need any adjustments.

2) I'm not aware of any external schedulers relying on this command yet.

So I think it's better to keep the protocol simple, not complicate it for
imaginary compatibility with some unknown and likely non-existent
schedulers.

Co-authored-by: Bruno Oliveira <nicoddemus@gmail.com>
2024-10-30 14:00:10 -03:00
pre-commit-ci[bot]
34c554987a [pre-commit.ci] pre-commit autoupdate (#1147)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.7.0 → v0.7.1](https://github.com/astral-sh/ruff-pre-commit/compare/v0.7.0...v0.7.1)
- [github.com/asottile/blacken-docs: 1.19.0 → 1.19.1](https://github.com/asottile/blacken-docs/compare/1.19.0...1.19.1)
- [github.com/pre-commit/mirrors-mypy: v1.12.1 → v1.13.0](https://github.com/pre-commit/mirrors-mypy/compare/v1.12.1...v1.13.0)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-10-28 22:46:37 -03:00
Sviatoslav Sydorenko (Святослав Сидоренко)
9c24f0fa4a Merge pull request #1143 from pytest-dev/pre-commit-ci-update-config 2024-10-22 01:30:42 +02:00
pre-commit-ci[bot]
def1f883ff [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.6.9 → v0.7.0](https://github.com/astral-sh/ruff-pre-commit/compare/v0.6.9...v0.7.0)
- [github.com/pre-commit/mirrors-mypy: v1.11.2 → v1.12.1](https://github.com/pre-commit/mirrors-mypy/compare/v1.11.2...v1.12.1)
2024-10-21 23:02:01 +00:00
Ronny Pfannschmidt
469f31bc2d Merge pull request #1142 from edgarrmondragon/cp313
Added support for Python 3.13
2024-10-20 11:00:37 +02:00
Edgar Ramírez-Mondragón
3bf62e0e22 Added changelog entry 2024-10-19 08:57:03 -06:00
Edgar Ramírez-Mondragón
1c60dafcb2 Added support for Python 3.13 2024-10-19 08:56:01 -06:00
pre-commit-ci[bot]
a65df239f4 [pre-commit.ci] pre-commit autoupdate (#1137)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.6.8 → v0.6.9](https://github.com/astral-sh/ruff-pre-commit/compare/v0.6.8...v0.6.9)
- [github.com/asottile/blacken-docs: 1.18.0 → 1.19.0](https://github.com/asottile/blacken-docs/compare/1.18.0...1.19.0)
- [github.com/pre-commit/pre-commit-hooks: v4.6.0 → v5.0.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.6.0...v5.0.0)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-10-08 06:43:26 +02:00
dependabot[bot]
c483298478 build(deps): bump pypa/gh-action-pypi-publish (#1136)
Bumps the github-actions group with 1 update: [pypa/gh-action-pypi-publish](https://github.com/pypa/gh-action-pypi-publish).


Updates `pypa/gh-action-pypi-publish` from 1.10.2 to 1.10.3
- [Release notes](https://github.com/pypa/gh-action-pypi-publish/releases)
- [Commits](https://github.com/pypa/gh-action-pypi-publish/compare/v1.10.2...v1.10.3)

---
updated-dependencies:
- dependency-name: pypa/gh-action-pypi-publish
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-07 23:02:42 +02:00
pre-commit-ci[bot]
3d1f4b1e04 [pre-commit.ci] pre-commit autoupdate (#1135)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.6.7 → v0.6.8](https://github.com/astral-sh/ruff-pre-commit/compare/v0.6.7...v0.6.8)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-10-01 10:31:46 +02:00
pre-commit-ci[bot]
cbbb894e71 [pre-commit.ci] pre-commit autoupdate (#1133)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.6.5 → v0.6.7](https://github.com/astral-sh/ruff-pre-commit/compare/v0.6.5...v0.6.7)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-09-24 06:48:26 +02:00
dependabot[bot]
8b82eafcd4 build(deps): bump pypa/gh-action-pypi-publish (#1132)
Bumps the github-actions group with 1 update: [pypa/gh-action-pypi-publish](https://github.com/pypa/gh-action-pypi-publish).


Updates `pypa/gh-action-pypi-publish` from 1.10.1 to 1.10.2
- [Release notes](https://github.com/pypa/gh-action-pypi-publish/releases)
- [Commits](https://github.com/pypa/gh-action-pypi-publish/compare/v1.10.1...v1.10.2)

---
updated-dependencies:
- dependency-name: pypa/gh-action-pypi-publish
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-23 20:02:46 +02:00
pre-commit-ci[bot]
a66354ada5 [pre-commit.ci] pre-commit autoupdate (#1129)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.6.4 → v0.6.5](https://github.com/astral-sh/ruff-pre-commit/compare/v0.6.4...v0.6.5)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-09-17 21:00:47 -03:00
pre-commit-ci[bot]
b05a5bc7b8 [pre-commit.ci] pre-commit autoupdate (#1128)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.6.3 → v0.6.4](https://github.com/astral-sh/ruff-pre-commit/compare/v0.6.3...v0.6.4)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-09-09 20:28:12 -03:00
dependabot[bot]
38656eacef build(deps): bump the github-actions group with 2 updates (#1127)
Bumps the github-actions group with 2 updates: [hynek/build-and-inspect-python-package](https://github.com/hynek/build-and-inspect-python-package) and [pypa/gh-action-pypi-publish](https://github.com/pypa/gh-action-pypi-publish).


Updates `hynek/build-and-inspect-python-package` from 2.8 to 2.9
- [Release notes](https://github.com/hynek/build-and-inspect-python-package/releases)
- [Changelog](https://github.com/hynek/build-and-inspect-python-package/blob/main/CHANGELOG.md)
- [Commits](https://github.com/hynek/build-and-inspect-python-package/compare/v2.8...v2.9)

Updates `pypa/gh-action-pypi-publish` from 1.10.0 to 1.10.1
- [Release notes](https://github.com/pypa/gh-action-pypi-publish/releases)
- [Commits](https://github.com/pypa/gh-action-pypi-publish/compare/v1.10.0...v1.10.1)

---
updated-dependencies:
- dependency-name: hynek/build-and-inspect-python-package
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
- dependency-name: pypa/gh-action-pypi-publish
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-09 18:22:54 -03:00
Bruno Oliveira
00231dce72 Publish package with attestations (#1125)
Follow up to https://github.com/pytest-dev/pytest-xdist/pull/1123.
2024-09-04 09:55:16 -03:00
pre-commit-ci[bot]
59866c7281 [pre-commit.ci] pre-commit autoupdate (#1124)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.6.2 → v0.6.3](https://github.com/astral-sh/ruff-pre-commit/compare/v0.6.2...v0.6.3)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-09-02 21:36:55 -03:00
dependabot[bot]
6f0620f559 build(deps): bump pypa/gh-action-pypi-publish (#1123)
Bumps the github-actions group with 1 update: [pypa/gh-action-pypi-publish](https://github.com/pypa/gh-action-pypi-publish).


Updates `pypa/gh-action-pypi-publish` from 1.9.0 to 1.10.0
- [Release notes](https://github.com/pypa/gh-action-pypi-publish/releases)
- [Commits](https://github.com/pypa/gh-action-pypi-publish/compare/v1.9.0...v1.10.0)

---
updated-dependencies:
- dependency-name: pypa/gh-action-pypi-publish
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-02 20:10:40 +02:00
pre-commit-ci[bot]
548280f737 [pre-commit.ci] pre-commit autoupdate (#1120)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.6.1 → v0.6.2](https://github.com/astral-sh/ruff-pre-commit/compare/v0.6.1...v0.6.2)
- [github.com/pre-commit/mirrors-mypy: v1.11.1 → v1.11.2](https://github.com/pre-commit/mirrors-mypy/compare/v1.11.1...v1.11.2)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-08-26 23:28:07 -03:00
pre-commit-ci[bot]
5a7e87f06b [pre-commit.ci] pre-commit autoupdate (#1117)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.5.7 → v0.6.1](https://github.com/astral-sh/ruff-pre-commit/compare/v0.5.7...v0.6.1)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-08-19 20:34:24 -03:00
pre-commit-ci[bot]
649cca56bb [pre-commit.ci] pre-commit autoupdate (#1116)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.5.6 → v0.5.7](https://github.com/astral-sh/ruff-pre-commit/compare/v0.5.6...v0.5.7)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-08-13 06:24:04 +02:00
pre-commit-ci[bot]
629f3164a9 [pre-commit.ci] pre-commit autoupdate (#1115)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.5.5 → v0.5.6](https://github.com/astral-sh/ruff-pre-commit/compare/v0.5.5...v0.5.6)
- [github.com/pre-commit/mirrors-mypy: v1.11.0 → v1.11.1](https://github.com/pre-commit/mirrors-mypy/compare/v1.11.0...v1.11.1)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-08-06 07:10:39 +02:00
dependabot[bot]
4902dc32e9 build(deps): bump hynek/build-and-inspect-python-package (#1112)
Bumps the github-actions group with 1 update: [hynek/build-and-inspect-python-package](https://github.com/hynek/build-and-inspect-python-package).


Updates `hynek/build-and-inspect-python-package` from 2.7 to 2.8
- [Release notes](https://github.com/hynek/build-and-inspect-python-package/releases)
- [Changelog](https://github.com/hynek/build-and-inspect-python-package/blob/main/CHANGELOG.md)
- [Commits](https://github.com/hynek/build-and-inspect-python-package/compare/v2.7...v2.8)

---
updated-dependencies:
- dependency-name: hynek/build-and-inspect-python-package
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-03 12:18:54 -03:00
pre-commit-ci[bot]
e377c3428c [pre-commit.ci] pre-commit autoupdate (#1113)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.5.4 → v0.5.5](https://github.com/astral-sh/ruff-pre-commit/compare/v0.5.4...v0.5.5)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-07-31 22:23:53 -03:00
Ronny Pfannschmidt
224d026e67 Merge pull request #1110 from pytest-dev/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2024-07-24 00:12:19 +02:00
Bruno Oliveira
569cb964d3 Adapt to mypy 1.11 2024-07-23 10:05:14 -03:00
pre-commit-ci[bot]
8230e4f1e1 [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.5.2 → v0.5.4](https://github.com/astral-sh/ruff-pre-commit/compare/v0.5.2...v0.5.4)
- [github.com/pre-commit/mirrors-mypy: v1.10.1 → v1.11.0](https://github.com/pre-commit/mirrors-mypy/compare/v1.10.1...v1.11.0)
2024-07-22 22:33:28 +00:00
dependabot[bot]
b417c792f1 build(deps): bump hynek/build-and-inspect-python-package (#1109)
Bumps the github-actions group with 1 update: [hynek/build-and-inspect-python-package](https://github.com/hynek/build-and-inspect-python-package).


Updates `hynek/build-and-inspect-python-package` from 2.6 to 2.7
- [Release notes](https://github.com/hynek/build-and-inspect-python-package/releases)
- [Changelog](https://github.com/hynek/build-and-inspect-python-package/blob/main/CHANGELOG.md)
- [Commits](https://github.com/hynek/build-and-inspect-python-package/compare/v2.6...v2.7)

---
updated-dependencies:
- dependency-name: hynek/build-and-inspect-python-package
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-22 15:35:56 -03:00
pre-commit-ci[bot]
d8efeb9f74 [pre-commit.ci] pre-commit autoupdate (#1105)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.5.0 → v0.5.2](https://github.com/astral-sh/ruff-pre-commit/compare/v0.5.0...v0.5.2)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-07-18 09:01:15 -03:00
dependabot[bot]
fabdbe3fd2 build(deps): bump pypa/gh-action-pypi-publish (#1100)
Bumps the github-actions group with 1 update: [pypa/gh-action-pypi-publish](https://github.com/pypa/gh-action-pypi-publish).


Updates `pypa/gh-action-pypi-publish` from 1.8.14 to 1.9.0
- [Release notes](https://github.com/pypa/gh-action-pypi-publish/releases)
- [Commits](https://github.com/pypa/gh-action-pypi-publish/compare/v1.8.14...v1.9.0)

---
updated-dependencies:
- dependency-name: pypa/gh-action-pypi-publish
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-02 08:41:58 -03:00
pre-commit-ci[bot]
f8eaa1483b [pre-commit.ci] pre-commit autoupdate (#1104)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.4.10 → v0.5.0](https://github.com/astral-sh/ruff-pre-commit/compare/v0.4.10...v0.5.0)
- [github.com/asottile/blacken-docs: 1.16.0 → 1.18.0](https://github.com/asottile/blacken-docs/compare/1.16.0...1.18.0)
- [github.com/pre-commit/mirrors-mypy: v1.10.0 → v1.10.1](https://github.com/pre-commit/mirrors-mypy/compare/v1.10.0...v1.10.1)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-07-02 07:55:25 -03:00
pre-commit-ci[bot]
f1ae09176b [pre-commit.ci] pre-commit autoupdate (#1101)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.4.8 → v0.4.10](https://github.com/astral-sh/ruff-pre-commit/compare/v0.4.8...v0.4.10)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-06-27 09:27:07 -03:00
pre-commit-ci[bot]
c7b4f61144 [pre-commit.ci] pre-commit autoupdate (#1095)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.4.7 → v0.4.8](https://github.com/astral-sh/ruff-pre-commit/compare/v0.4.7...v0.4.8)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-06-10 21:58:13 -03:00
dependabot[bot]
39cb171341 build(deps): bump hynek/build-and-inspect-python-package (#1088)
Bumps the github-actions group with 1 update: [hynek/build-and-inspect-python-package](https://github.com/hynek/build-and-inspect-python-package).


Updates `hynek/build-and-inspect-python-package` from 2.5 to 2.6
- [Release notes](https://github.com/hynek/build-and-inspect-python-package/releases)
- [Changelog](https://github.com/hynek/build-and-inspect-python-package/blob/main/CHANGELOG.md)
- [Commits](https://github.com/hynek/build-and-inspect-python-package/compare/v2.5...v2.6)

---
updated-dependencies:
- dependency-name: hynek/build-and-inspect-python-package
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-10 21:58:00 -03:00
pre-commit-ci[bot]
98f2fc5982 [pre-commit.ci] pre-commit autoupdate (#1079)
* [pre-commit.ci] pre-commit autoupdate

updates:
- [github.com/astral-sh/ruff-pre-commit: v0.4.1 → v0.4.7](https://github.com/astral-sh/ruff-pre-commit/compare/v0.4.1...v0.4.7)
- [github.com/pre-commit/mirrors-mypy: v1.9.0 → v1.10.0](https://github.com/pre-commit/mirrors-mypy/compare/v1.9.0...v1.10.0)

* Ignore "Use format specifiers instead of percent format" ruff error

No reason to force this in old code.

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Bruno Oliveira <nicoddemus@gmail.com>
2024-06-04 09:59:02 -03:00
Nick Murphy
6f3f574c90 Clarify error message to indicate that users should refer to the documentation of pytest-xdist (#1092) 2024-06-04 08:50:20 -03:00
dependabot[bot]
7cc6515c06 build(deps): bump hynek/build-and-inspect-python-package (#1082)
Bumps the github-actions group with 1 update: [hynek/build-and-inspect-python-package](https://github.com/hynek/build-and-inspect-python-package).


Updates `hynek/build-and-inspect-python-package` from 2.4 to 2.5
- [Release notes](https://github.com/hynek/build-and-inspect-python-package/releases)
- [Changelog](https://github.com/hynek/build-and-inspect-python-package/blob/main/CHANGELOG.md)
- [Commits](https://github.com/hynek/build-and-inspect-python-package/compare/v2.4...v2.5)

---
updated-dependencies:
- dependency-name: hynek/build-and-inspect-python-package
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-16 07:38:54 -03:00
Ran Benita
e04e4ac30d Merge pull request #1077 from bluetech/tox-tweak
Some tox/build/config tweaks
2024-04-29 10:09:07 +03:00
Ran Benita
64469f56a3 MANIFEST.in: remove
I don't think a few unnecessary dot files bother anyone, and may
arguably be useful to someone.
2024-04-28 22:59:43 +03:00
Ran Benita
7c83841a3d pyproject: remove setuptools include-package-data = false
I compared the sdists and wheels and it makes no difference.
2024-04-28 22:54:31 +03:00
Ran Benita
6d54a6b2e3 pyproject: remove setuptools zip-safe key
According to
https://setuptools.pypa.io/en/latest/userguide/pyproject_config.html#setuptools-specific-configuration
it's no longer relevant.
2024-04-28 22:52:30 +03:00
Ran Benita
a9a3d13a29 pyproject: remove setuptools platforms key
According to
https://packaging.python.org/en/latest/specifications/core-metadata/#platform-multiple-use
this is redundant with the classifiers.
2024-04-28 22:51:26 +03:00
Ran Benita
d204fdf965 pyproject: move pytest config from tox.ini 2024-04-28 22:48:51 +03:00
Ran Benita
fb51e6ba20 tox: some cleanups & remove hardcoded python3.10 2024-04-28 22:46:55 +03:00
Ran Benita
c143230d88 Merge pull request #1076 from pytest-dev/release-3.6.1
Release 3.6.1
2024-04-28 22:30:35 +03:00
Ran Benita
4dd2978031 Release 3.6.1 2024-04-28 22:20:45 +03:00
Ran Benita
b397288b7e Merge pull request #1072 from zmedico/gateway-cache-rinfo
Cache execnet gateway rinfo during WorkerController setup
2024-04-28 22:17:03 +03:00
Zac Medico
12b3cce0ce Cache execnet gateway rinfo during WorkerController setup
Cache execnet gateway info during WorkerController setup for backward
compatibility, in order to avoid a later main_thread_only deadlock
error triggered when pytest-cov calls rinfo after the main thread is
already busy. See pytest-dev/execnet#274 for corresponding test case.

Fixes: 20e3ac774e ("Use execnet main_thread_only execmodel (#1027)")
2024-04-28 22:01:32 +03:00
dependabot[bot]
c93a106b3a build(deps): bump hynek/build-and-inspect-python-package (#1066)
Bumps the github-actions group with 1 update: [hynek/build-and-inspect-python-package](https://github.com/hynek/build-and-inspect-python-package).


Updates `hynek/build-and-inspect-python-package` from 2.2 to 2.4
- [Release notes](https://github.com/hynek/build-and-inspect-python-package/releases)
- [Changelog](https://github.com/hynek/build-and-inspect-python-package/blob/main/CHANGELOG.md)
- [Commits](https://github.com/hynek/build-and-inspect-python-package/compare/v2.2...v2.4)

---
updated-dependencies:
- dependency-name: hynek/build-and-inspect-python-package
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-23 14:20:59 -03:00
pre-commit-ci[bot]
52e2022638 [pre-commit.ci] pre-commit autoupdate (#1073)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.3.7 → v0.4.1](https://github.com/astral-sh/ruff-pre-commit/compare/v0.3.7...v0.4.1)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-04-23 14:20:44 -03:00
Ran Benita
699f939b5c Merge pull request #1070 from pytest-dev/release-3.6.0
Release 3.6.0
2024-04-19 22:49:28 +03:00
Ran Benita
80bc0b8e5b Release 3.6.0 2024-04-19 22:39:59 +03:00
Zac Medico
20e3ac774e Use execnet main_thread_only execmodel (#1027)
Use the execnet main_thread_only execmodel so that code which expects
to run in the main thread will just work.  This execmodel has been
merged to the execnet master branch via pytest-dev/execnet#243, so this
patch should not be merged until there is a released version of execnet
supporting the main_thread_only execmodel.

Closes #620
2024-04-19 08:03:32 -03:00
Bruno Oliveira
0a4238f6da Merge pull request #1067 from pytest-dev/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2024-04-17 15:11:33 -03:00
pre-commit-ci[bot]
068627994f [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.3.5 → v0.3.7](https://github.com/astral-sh/ruff-pre-commit/compare/v0.3.5...v0.3.7)
2024-04-17 15:09:14 -03:00
Ran Benita
ecf7c1cd10 Merge pull request #1065 from bluetech/typing
Improve typing
2024-04-16 23:36:41 +03:00
Ran Benita
b78cf1c0ce Improve typing
Fix #1057
2024-04-15 23:30:17 +03:00
Ran Benita
5dfc590a8c Merge pull request #1061 from bluetech/scheduling-proto
Add a Scheduling Protocol
2024-04-15 18:20:29 +03:00
Ran Benita
bf2dcccfb0 Add a Scheduling Protocol
- Clearly delineate the required interface of schedulers
- Useful for typing
2024-04-15 18:05:46 +03:00
Bruno Oliveira
91812bf5f9 Merge pull request #1062 from pytest-dev/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2024-04-10 11:08:51 -03:00
pre-commit-ci[bot]
7c3f70f007 [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/pre-commit/pre-commit-hooks: v4.5.0 → v4.6.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.5.0...v4.6.0)
2024-04-08 22:37:10 +00:00
Ran Benita
47208081d6 Merge pull request #1060 from bluetech/pre-typing-fixes
Pre-typing fixes/improvements
2024-04-06 22:26:31 +03:00
Ran Benita
cdff86a131 pyproject: make plain mypy run work 2024-04-06 21:55:44 +03:00
Ran Benita
29aa05f9bf testing: add worksteal to TestLocking tests
Seems like it intends to check all schedulers.
2024-04-06 21:55:44 +03:00
Ran Benita
d6c81d5537 pre-commit: remove py from typing deps, add types-psutil
py is no longer used.
psutil is used optionally so add its typing.
2024-04-06 21:55:42 +03:00
Ran Benita
e7a971b167 Use from __future__ import annotations
Allows us to use more modern typing.
2024-04-06 21:55:21 +03:00
Ran Benita
059c1bcc8c remote,workermanage: use enums for markers
Enum has a unique type, unlike `object()`, enabling better typing.
2024-04-06 21:54:44 +03:00
Ran Benita
ee0b09c61f loadscope: use dict instead of OrderedDict
These days dicts are guaranteed to be ordered, so no need to use
OrderedDict in two of the three cases (the remaining case needs
`popitem(last=False)`).
2024-04-06 21:54:44 +03:00
Ran Benita
024c73f29e testing: fix a private pytest import to its current location 2024-04-06 21:54:44 +03:00
Ran Benita
5c676aa685 dsession,workermanage: drop handling of pytest_captured_warning
Since d153e0a4c4 the remote doesn't send
events for this hook at all (I think perhaps wrongly, but it's history
by now), so no point in handling it in the coordinator side.
2024-04-06 21:54:44 +03:00
Ran Benita
a3b9a98126 looponfail: remove unneeded setup(out) parameter
It's not used.
2024-04-06 21:54:44 +03:00
Ran Benita
4057da1c1f remote: use perf_counter() to measure duration, not time()
`time()` is not monotonic and is not appropriate for measuring duration.
2024-04-06 21:54:44 +03:00
Ran Benita
ef5dcde977 each: remove incorrect mark_test_pending implementation
This code is bogus copy/paste; presumably it's not executed in practice
so make it raise `NotImplementedError`.
2024-04-06 21:54:44 +03:00
Ran Benita
503c5ee828 loadscope: remove redundant None check
self.config can't be None.
2024-04-06 21:54:44 +03:00
Ran Benita
8e77d14f8c testing: fix an assert
The assert as written was always true, according to the name `failures`
it intended the first element of the tuple.
2024-04-06 21:54:44 +03:00
Ran Benita
5dcc10c723 dsession: remove unnecessary getplugin("runner") 2024-04-06 21:54:44 +03:00
Ran Benita
3e9260462d Merge pull request #1059 from bluetech/execnet-min
Bump min execnet to 2.1.0
2024-04-06 21:51:30 +03:00
Ran Benita
e9084f2d73 Bump min execnet to 2.1.0
This version adds typing which we want to utilize.
2024-04-05 21:52:13 +03:00
Ran Benita
dd067e4f86 Merge pull request #1058 from bluetech/pytest-min-7
Bump min pytest to 7.0.0
2024-04-05 20:39:24 +03:00
Ran Benita
31de42ffa5 Bump min pytest to 7.0.0
Refs #1057
2024-04-05 11:50:11 +03:00
Ran Benita
99b767b6dd Merge pull request #1056 from bluetech/ci-pytestmin
ci: add a job for min supported pytest version
2024-04-05 10:58:02 +03:00
Ran Benita
fbf2eca13d ci: add a job for min supported pytest version
Make sure we don't break the promise.
2024-04-04 19:39:16 +03:00
Ran Benita
470bc83a25 Merge pull request #1052 from bluetech/pytester
testing: replace `testdir` -> `pytester`
2024-04-04 00:09:41 +03:00
Ronny Pfannschmidt
d96d87ef6f Merge pull request #1054 from bluetech/rm-py37
Drop support for Python 3.7
2024-04-03 21:56:21 +02:00
Ran Benita
a790dc5d72 Drop support for Python 3.7
Fix #1053
2024-04-03 18:07:38 +03:00
Ran Benita
d63400e8a6 testing: replace testdir -> pytester
The `testdir` fixture is soft-deprecated in favor of `pytester`.
2024-04-03 18:01:00 +03:00
Ran Benita
b6f5d2bd32 Merge pull request #1051 from bluetech/ruff
Use ruff instead of black, flake8, autoflake, pyupgrade
2024-04-03 09:42:26 +03:00
Ran Benita
816c9dcda1 Use ruff instead of black, flake8, autoflake, pyupgrade
Config adapted from pytest.
2024-04-03 00:19:32 +03:00
Ran Benita
c01de1c73e Make docstring style more consistent 2024-04-03 00:19:31 +03:00
Ran Benita
e3b69d4f29 Sort imports
Using pytest's style.
2024-04-03 00:19:31 +03:00
Bruno Oliveira
8f7bd68566 Merge pull request #1050 from pytest-dev/dependabot/github_actions/github-actions-867643949f
Bump the github-actions group with 1 update
2024-04-01 16:44:44 -03:00
dependabot[bot]
940c4fcf1d Bump the github-actions group with 1 update
Bumps the github-actions group with 1 update: [hynek/build-and-inspect-python-package](https://github.com/hynek/build-and-inspect-python-package).


Updates `hynek/build-and-inspect-python-package` from 2.0 to 2.2
- [Release notes](https://github.com/hynek/build-and-inspect-python-package/releases)
- [Changelog](https://github.com/hynek/build-and-inspect-python-package/blob/main/CHANGELOG.md)
- [Commits](https://github.com/hynek/build-and-inspect-python-package/compare/v2.0...v2.2)

---
updated-dependencies:
- dependency-name: hynek/build-and-inspect-python-package
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-01 17:25:28 +00:00
Ben Brown
84df4451ed Terminate the test loop if shouldfail or shouldstop is set (#1026) 2024-04-01 18:00:50 +03:00
Bruno Oliveira
dde8a66da5 Merge pull request #1047 from bluetech/rm-invocationparams-compat
Remove pytest InvocationParams backward compat
2024-04-01 07:36:30 -03:00
pre-commit-ci[bot]
74f5752ed9 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2024-03-23 18:42:36 +00:00
Ran Benita
00dc8eeaff Remove pytest InvocationParams backward compat
It's available since pytest 5.2, and pytest-xdist requires pytest>=6.2.
2024-03-23 20:40:54 +02:00
pre-commit-ci[bot]
74edbeb7c7 [pre-commit.ci] pre-commit autoupdate (#1044)
updates:
- [github.com/PyCQA/autoflake: v2.3.0 → v2.3.1](https://github.com/PyCQA/autoflake/compare/v2.3.0...v2.3.1)
- [github.com/psf/black: 24.2.0 → 24.3.0](https://github.com/psf/black/compare/24.2.0...24.3.0)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-03-19 12:27:16 -03:00
pre-commit-ci[bot]
5b83de49c5 [pre-commit.ci] pre-commit autoupdate (#1039)
updates:
- [github.com/pre-commit/mirrors-mypy: v1.8.0 → v1.9.0](https://github.com/pre-commit/mirrors-mypy/compare/v1.8.0...v1.9.0)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-03-13 06:55:47 -03:00
dependabot[bot]
a9801a4214 Bump the github-actions group with 2 updates (#1037)
Bumps the github-actions group with 2 updates: [pypa/gh-action-pypi-publish](https://github.com/pypa/gh-action-pypi-publish) and [softprops/action-gh-release](https://github.com/softprops/action-gh-release).


Updates `pypa/gh-action-pypi-publish` from 1.8.12 to 1.8.14
- [Release notes](https://github.com/pypa/gh-action-pypi-publish/releases)
- [Commits](https://github.com/pypa/gh-action-pypi-publish/compare/v1.8.12...v1.8.14)

Updates `softprops/action-gh-release` from 1 to 2
- [Release notes](https://github.com/softprops/action-gh-release/releases)
- [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md)
- [Commits](https://github.com/softprops/action-gh-release/compare/v1...v2)

---
updated-dependencies:
- dependency-name: pypa/gh-action-pypi-publish
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
- dependency-name: softprops/action-gh-release
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-12 09:52:45 -03:00
Albert Tugushev
74887e356d Fix incompatibility error with --pdb despite using -n0 (#938)
Fixes #937
2024-03-05 19:23:49 -03:00
dependabot[bot]
0596f2755b Bump the github-actions group with 1 update (#1035)
Bumps the github-actions group with 1 update: [pypa/gh-action-pypi-publish](https://github.com/pypa/gh-action-pypi-publish).


Updates `pypa/gh-action-pypi-publish` from 1.8.11 to 1.8.12
- [Release notes](https://github.com/pypa/gh-action-pypi-publish/releases)
- [Commits](https://github.com/pypa/gh-action-pypi-publish/compare/v1.8.11...v1.8.12)

---
updated-dependencies:
- dependency-name: pypa/gh-action-pypi-publish
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-04 15:15:58 -03:00
Ronny Pfannschmidt
54720c14f0 Merge pull request #1030 from jeremy-hiatt/dont-rewrite-path-hooks
Avoid modifying path placeholders created by editable installs
2024-03-01 07:34:14 +01:00
Jeremy Hiatt
79fb6ff9c0 testing: Add test to assert path hooks ignored in sys.path 2024-02-29 14:42:25 -08:00
Jeremy Hiatt
51ef94439c Avoid modifying path placeholders created by editable installs
The setuptools implementation of editable installs will insert a
placeholder entry into sys.path as part of its magic to register its
custom import mechanism.

These are not real filesystem paths and as such should not be
rewritten to absolute paths.
2024-02-28 15:17:12 -08:00
Bruno Oliveira
f57c65839b Fix '-n logical' inconsistencies and review --help formatting (#1022)
Fix #1021 and review formatting of the --help messages, following pytest's rules for consistency.
2024-02-25 11:49:41 -03:00
pre-commit-ci[bot]
ce9b01e50c [pre-commit.ci] pre-commit autoupdate (#1025)
updates:
- [github.com/PyCQA/autoflake: v2.2.1 → v2.3.0](https://github.com/PyCQA/autoflake/compare/v2.2.1...v2.3.0)
- [github.com/asottile/pyupgrade: v3.15.0 → v3.15.1](https://github.com/asottile/pyupgrade/compare/v3.15.0...v3.15.1)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-02-19 22:46:24 -03:00
pre-commit-ci[bot]
6ae08a04d2 [pre-commit.ci] pre-commit autoupdate (#1023)
updates:
- [github.com/psf/black: 24.1.1 → 24.2.0](https://github.com/psf/black/compare/24.1.1...24.2.0)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-02-13 13:28:30 -03:00
Michael McClelland
8d57046bd4 Update LICENSE formatting (#996)
Fixes issue with license file format to allow security scanners such as Black Duck to correctly identify the license type of this project as MIT.

Co-authored-by: Bruno Oliveira <bruno@soliv.dev>
2024-02-12 12:04:26 -03:00
Matthias Köppe
7132649eb5 Migrate to pyproject.toml (#1020)
Co-authored-by: Bruno Oliveira <bruno@soliv.dev>
2024-02-12 11:53:46 -03:00
dependabot[bot]
fe8c5a2012 Bump the github-actions group with 5 updates (#1016)
Bumps the github-actions group with 5 updates:

| Package | From | To |
| --- | --- | --- |
| [actions/checkout](https://github.com/actions/checkout) | `3` | `4` |
| [hynek/build-and-inspect-python-package](https://github.com/hynek/build-and-inspect-python-package) | `1.5` | `2.0` |
| [actions/download-artifact](https://github.com/actions/download-artifact) | `3` | `4` |
| [actions/setup-python](https://github.com/actions/setup-python) | `2` | `5` |
| [pypa/gh-action-pypi-publish](https://github.com/pypa/gh-action-pypi-publish) | `1.8.5` | `1.8.11` |


Updates `actions/checkout` from 3 to 4
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v3...v4)

Updates `hynek/build-and-inspect-python-package` from 1.5 to 2.0
- [Release notes](https://github.com/hynek/build-and-inspect-python-package/releases)
- [Changelog](https://github.com/hynek/build-and-inspect-python-package/blob/main/CHANGELOG.md)
- [Commits](https://github.com/hynek/build-and-inspect-python-package/compare/v1.5...v2.0)

Updates `actions/download-artifact` from 3 to 4
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v3...v4)

Updates `actions/setup-python` from 2 to 5
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v2...v5)

Updates `pypa/gh-action-pypi-publish` from 1.8.5 to 1.8.11
- [Release notes](https://github.com/pypa/gh-action-pypi-publish/releases)
- [Commits](https://github.com/pypa/gh-action-pypi-publish/compare/v1.8.5...v1.8.11)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: hynek/build-and-inspect-python-package
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: actions/download-artifact
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: actions/setup-python
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: pypa/gh-action-pypi-publish
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-31 15:56:27 -03:00
Dmitry Volkov
c2d31f5e8b Fix copyright year in project documentation (#898)
As per https://stackoverflow.com/questions/2390230/do-copyright-dates-need-to-be-updated, updated it to the first commit of this repository.
2024-01-31 15:44:46 -03:00
Christian Clauss
5ebf62a8bf Keep GitHub Actions up to date with GitHub's Dependabot (#1015)
Creates automatic pull requests to upgrade GitHub Actions
* https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot
* https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#package-ecosystem
2024-01-31 15:41:13 -03:00
pre-commit-ci[bot]
3f27fd6a23 [pre-commit.ci] pre-commit autoupdate (#1013)
* [pre-commit.ci] pre-commit autoupdate

updates:
- [github.com/psf/black: 23.12.1 → 24.1.1](https://github.com/psf/black/compare/23.12.1...24.1.1)

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-01-29 20:12:34 -03:00
pre-commit-ci[bot]
85421b926a [pre-commit.ci] pre-commit autoupdate (#1000)
updates:
- [github.com/PyCQA/flake8: 6.1.0 → 7.0.0](https://github.com/PyCQA/flake8/compare/6.1.0...7.0.0)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-01-09 21:12:48 -03:00
Ronny Pfannschmidt
56a1a11f2a Merge pull request #1003 from pytest-dev/fix-socketserver-link
Fix link to socketserver.py in the docs
2024-01-09 22:03:48 +01:00
Bruno Oliveira
b17011d098 Fix link to socketserver.py in the docs
Fix #1001
2024-01-09 17:34:16 -03:00
Ronny Pfannschmidt
f5573149fd Merge pull request #994 from pytest-dev/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2023-12-26 10:30:06 +01:00
pre-commit-ci[bot]
2d5f89ffc8 [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/psf/black: 23.12.0 → 23.12.1](https://github.com/psf/black/compare/23.12.0...23.12.1)
- [github.com/pre-commit/mirrors-mypy: v1.7.1 → v1.8.0](https://github.com/pre-commit/mirrors-mypy/compare/v1.7.1...v1.8.0)
2023-12-25 20:43:44 +00:00
pre-commit-ci[bot]
a5848d11b8 [pre-commit.ci] pre-commit autoupdate (#990)
updates:
- [github.com/psf/black: 23.11.0 → 23.12.0](https://github.com/psf/black/compare/23.11.0...23.12.0)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-12-19 07:46:54 -03:00
Bruno Oliveira
ef344e9b55 Add deploy instructions using the command-line (#967) 2023-12-01 11:28:21 -03:00
Ronny Pfannschmidt
5ec4d37f20 Merge pull request #978 from pytest-dev/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2023-11-27 22:21:26 +01:00
pre-commit-ci[bot]
cd7383bedb [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/pre-commit/mirrors-mypy: v1.7.0 → v1.7.1](https://github.com/pre-commit/mirrors-mypy/compare/v1.7.0...v1.7.1)
2023-11-27 20:21:38 +00:00
Bruno Oliveira
f67501bb26 Merge pull request #974 from pytest-dev/release-3.5.0
Release 3.5.0
2023-11-21 13:02:15 -03:00
Bruno Oliveira
95b309e980 Release 3.5.0 2023-11-21 11:33:54 -03:00
Henk-Jaap Wagenaar
3fe877bc6d LoadScope scheduler: Sort scopes by number of tests to assign biggest scopes first (#778)
Follow up to # 632.

---------

Co-authored-by: Jörg Kohlsdorf <joerg@cornershopapp.com>
2023-11-21 11:32:29 -03:00
Bruno Oliveira
f36ea25cb5 Create GitHub release after deploy (#973)
Fixes #972
2023-11-21 11:26:23 -03:00
Bruno Oliveira
d81f57508d Merge pull request #969 from pytest-dev/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2023-11-14 08:43:15 -03:00
pre-commit-ci[bot]
1d2616418c [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/psf/black: 23.10.1 → 23.11.0](https://github.com/psf/black/compare/23.10.1...23.11.0)
- [github.com/pre-commit/mirrors-mypy: v1.6.1 → v1.7.0](https://github.com/pre-commit/mirrors-mypy/compare/v1.6.1...v1.7.0)
2023-11-13 21:01:19 +00:00
Bruno Oliveira
5f43575b42 Merge pull request #966 from pytest-dev/release-3.4.0
Release 3.4.0
2023-11-11 13:10:39 -03:00
Bruno Oliveira
a6b56112f9 Update CHANGELOG 2023-11-11 12:13:49 -03:00
Bruno Oliveira
64f9388f41 Add support for Python 3.12
Fix changelog entries from "improvement" to "feature" (there's "improvement" in our towncrier configuration).
2023-11-11 12:13:49 -03:00
Paul Ollis
230ba6ad10 Properly wait for workers when test run terminates early (#963)
Currently, a reason to terminate early (e.g. test failure with --exitfail
option set) causes DSession to immediately raise an Interrupt exception.
Subsequent reports generated by the workers, during the shutdown phase, are
discarded.

One consequence is that, for the failing test, teardown and testfinish are
ignored, which prevents corresponding hooks pytest_runtest_logreport and
pytest_runtest_logfinish being executed for the failing test (this problem
covered in #54). The reporting of tests executing in other workers is also left in an
indeterminate state. This can affect other plugin code.

This is a relatively simple fix, which appears to have minimal and, I think,
acceptable impact on text execution behaviour.

The observable differences are differences in what is reported about a test
run. For example, when running, for example with the '-x/--exitfail' option, it
is possible that more than a single test failure is reported. This is because
more than one test did fail before the test run was completely stopped. The
reporting is absolutely correct; and complete. Prior to this change, only a
single failure would have been reported, but because of incomplete and arguably
incorrect reporting.
2023-11-11 11:24:21 -03:00
Phillip Yen
93ca202b40 fix typo index -> instead 2023-11-04 14:24:08 -03:00
pre-commit-ci[bot]
8f3d1ad92f [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/psf/black: 23.9.1 → 23.10.1](https://github.com/psf/black/compare/23.9.1...23.10.1)
- [github.com/pre-commit/mirrors-mypy: v1.6.0 → v1.6.1](https://github.com/pre-commit/mirrors-mypy/compare/v1.6.0...v1.6.1)
2023-10-24 08:34:08 -03:00
Bruno Oliveira
ab3d6a9ee5 Merge pull request #955 from pytest-dev/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2023-10-17 09:08:19 -03:00
pre-commit-ci[bot]
a5210d4103 [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/pre-commit/mirrors-mypy: v1.5.1 → v1.6.0](https://github.com/pre-commit/mirrors-mypy/compare/v1.5.1...v1.6.0)
2023-10-16 20:24:41 +00:00
Bruno Oliveira
0b6b9c3c38 Merge pull request #954 from pytest-dev/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2023-10-10 08:15:59 -03:00
Bruno Oliveira
838434c269 Install requirements.txt while building docs 2023-10-10 08:14:19 -03:00
Bruno Oliveira
b85a4717f9 Add docs/requirements.txt 2023-10-10 08:13:46 -03:00
pre-commit-ci[bot]
b2770e65e1 [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/pre-commit/pre-commit-hooks: v4.4.0 → v4.5.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.4.0...v4.5.0)
- [github.com/asottile/pyupgrade: v3.14.0 → v3.15.0](https://github.com/asottile/pyupgrade/compare/v3.14.0...v3.15.0)
2023-10-09 21:00:06 +00:00
pre-commit-ci[bot]
4a3a8b499b [pre-commit.ci] pre-commit autoupdate (#953)
updates:
- [github.com/asottile/pyupgrade: v3.13.0 → v3.14.0](https://github.com/asottile/pyupgrade/compare/v3.13.0...v3.14.0)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-10-03 09:28:00 -03:00
pre-commit-ci[bot]
4016c2a173 [pre-commit.ci] pre-commit autoupdate (#951)
updates:
- [github.com/asottile/pyupgrade: v3.11.0 → v3.13.0](https://github.com/asottile/pyupgrade/compare/v3.11.0...v3.13.0)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-09-26 06:58:37 -03:00
pre-commit-ci[bot]
ddc28f3870 [pre-commit.ci] pre-commit autoupdate (#949)
updates:
- [github.com/asottile/pyupgrade: v3.10.1 → v3.11.0](https://github.com/asottile/pyupgrade/compare/v3.10.1...v3.11.0)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-09-19 10:10:24 -03:00
pre-commit-ci[bot]
02ddfc9860 [pre-commit.ci] pre-commit autoupdate (#948)
updates:
- [github.com/psf/black: 23.7.0 → 23.9.1](https://github.com/psf/black/compare/23.7.0...23.9.1)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-09-12 07:50:26 -03:00
Bruno Oliveira
f15de24e5a Merge pull request #946 from pytest-dev/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2023-09-05 07:24:48 -03:00
Bruno Oliveira
49a1093574 Add readthedocs configuration file -- now required 2023-09-05 07:18:41 -03:00
Bruno Oliveira
470f4ee53c Fix warn_on_impl parameter on newhooks.py 2023-09-05 07:18:11 -03:00
pre-commit-ci[bot]
733d769bc0 [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/PyCQA/autoflake: v2.2.0 → v2.2.1](https://github.com/PyCQA/autoflake/compare/v2.2.0...v2.2.1)
2023-09-05 08:25:26 +00:00
pre-commit-ci[bot]
8ff5c62cfe [pre-commit.ci] pre-commit autoupdate (#941)
updates:
- [github.com/asottile/blacken-docs: 1.15.0 → 1.16.0](https://github.com/asottile/blacken-docs/compare/1.15.0...1.16.0)
- [github.com/pre-commit/mirrors-mypy: v1.5.0 → v1.5.1](https://github.com/pre-commit/mirrors-mypy/compare/v1.5.0...v1.5.1)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-08-26 16:38:37 -03:00
Bruno Oliveira
6b52e0a23d Merge pull request #939 from pytest-dev/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2023-08-15 09:26:03 -03:00
pre-commit-ci[bot]
d5313c5660 [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/pre-commit/mirrors-mypy: v1.4.1 → v1.5.0](https://github.com/pre-commit/mirrors-mypy/compare/v1.4.1...v1.5.0)
2023-08-15 07:24:55 +00:00
Bruno Oliveira
bfd28ff398 Merge pull request #934 from pytest-dev/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2023-08-02 09:51:08 -03:00
Ronny Pfannschmidt
17da7cbe59 correct the type compare the updated flake8 found 2023-08-01 09:12:00 +00:00
pre-commit-ci[bot]
caff114004 [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/PyCQA/flake8: 6.0.0 → 6.1.0](https://github.com/PyCQA/flake8/compare/6.0.0...6.1.0)
- [github.com/asottile/pyupgrade: v3.9.0 → v3.10.1](https://github.com/asottile/pyupgrade/compare/v3.9.0...v3.10.1)
2023-08-01 08:52:03 +00:00
pre-commit-ci[bot]
6f0cb26fc0 [pre-commit.ci] pre-commit autoupdate (#929)
updates:
- [github.com/psf/black: 23.3.0 → 23.7.0](https://github.com/psf/black/compare/23.3.0...23.7.0)
- [github.com/asottile/blacken-docs: 1.14.0 → 1.15.0](https://github.com/asottile/blacken-docs/compare/1.14.0...1.15.0)
- [github.com/asottile/pyupgrade: v3.8.0 → v3.9.0](https://github.com/asottile/pyupgrade/compare/v3.8.0...v3.9.0)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-07-11 07:58:21 -03:00
Bruno Oliveira
5661f498c7 Docs: clarify that only rsync support is deprecated (#927)
Related to #839
2023-07-10 07:53:38 -03:00
Ronny Pfannschmidt
380f978243 Merge pull request #926 from pytest-dev/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2023-07-04 09:29:28 +02:00
pre-commit-ci[bot]
f609fb7de4 [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/asottile/pyupgrade: v3.7.0 → v3.8.0](https://github.com/asottile/pyupgrade/compare/v3.7.0...v3.8.0)
2023-07-04 06:41:04 +00:00
pre-commit-ci[bot]
03a64d91f3 [pre-commit.ci] pre-commit autoupdate (#923)
updates:
- [github.com/PyCQA/autoflake: v2.1.1 → v2.2.0](https://github.com/PyCQA/autoflake/compare/v2.1.1...v2.2.0)
- [github.com/pre-commit/mirrors-mypy: v1.3.0 → v1.4.1](https://github.com/pre-commit/mirrors-mypy/compare/v1.3.0...v1.4.1)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-06-27 08:07:12 -03:00
pre-commit-ci[bot]
7d14d50f65 [pre-commit.ci] pre-commit autoupdate (#920)
updates:
- [github.com/asottile/blacken-docs: 1.13.0 → 1.14.0](https://github.com/asottile/blacken-docs/compare/1.13.0...1.14.0)
- [github.com/asottile/pyupgrade: v3.6.0 → v3.7.0](https://github.com/asottile/pyupgrade/compare/v3.6.0...v3.7.0)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-06-20 07:56:49 -03:00
Bruno Oliveira
56a4f212b7 Merge pull request #918 from pytest-dev/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2023-06-13 08:22:42 -03:00
pre-commit-ci[bot]
a4c887b058 [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/asottile/pyupgrade: v3.4.0 → v3.6.0](https://github.com/asottile/pyupgrade/compare/v3.4.0...v3.6.0)
2023-06-13 05:13:50 +00:00
Bruno Oliveira
7e1768f838 Merge pull request #910 from pytest-dev/release-3.3.1
Release 3.3.1
2023-05-19 07:55:03 -03:00
Bruno Oliveira
9791021f02 Release 3.3.1 2023-05-19 07:44:21 -03:00
Bruno Oliveira
4e7bd0239e Avoid unnecessary rinfo calls after creating gateways (#909)
Hopefully this fixes #907, as seems this is the only
change in #901 which is somehow related.

---------

Co-authored-by: Ronny Pfannschmidt <opensource@ronnypfannschmidt.de>
2023-05-19 07:43:18 -03:00
pre-commit-ci[bot]
52a61434fb [pre-commit.ci] pre-commit autoupdate (#908)
updates:
- [github.com/pre-commit/mirrors-mypy: v1.2.0 → v1.3.0](https://github.com/pre-commit/mirrors-mypy/compare/v1.2.0...v1.3.0)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-05-16 06:20:14 -03:00
Bruno Oliveira
9efc64eef9 Merge pull request #906 from nicoddemus/ci-improvements
CI improvements
2023-05-15 08:21:30 -03:00
Bruno Oliveira
6d39025be3 Use same package in all test runs 2023-05-12 17:59:46 -03:00
Bruno Oliveira
d8fb60ba7f Fix tag creation in deploy workflow
As commented in #905, an annotated tag needs a configured user.
2023-05-12 17:52:21 -03:00
Bruno Oliveira
4d230b6de7 Merge pull request #905 from pytest-dev/release-3.3.0
Release 3.3.0
2023-05-12 17:48:57 -03:00
Bruno Oliveira
f5b4a5d6ed Configure 'test' workflow to cancel running jobs for the branch 2023-05-12 17:33:18 -03:00
Bruno Oliveira
657ba9ac7f Only test on-push for master and a specific branch name
This avoids testing twice when pushing to the main repository, once for the push, and another for a PR.
2023-05-12 17:33:18 -03:00
Bruno Oliveira
fa08e0987b Add check-package step also to 'test' workflow
This ensures we do not have any surprises when we try to deploy,
which uses the same step to generate the package.
2023-05-12 17:33:18 -03:00
Bruno Oliveira
103ee05f01 Release 3.3.0 2023-05-12 12:24:43 -03:00
Bruno Oliveira
37b9dbdbf4 Deploy via a protected environment and using PyPI trusted publishers (#902)
Following recent discussions, this changes the development process as follows:

1. The deploy is now manually triggered after the release PR is approved.
2. The deploy workflow tags the repository only after the package has been published to PyPI.
3. Use PyPI trusted publishers instead of API tokens.


Co-authored-by: Ran Benita <ran@unusedvar.com>
2023-05-12 12:17:55 -03:00
Bruno Oliveira
be1d5c401f Make collection progress output less verbose (#901)
Fix #555
2023-05-12 10:36:32 -03:00
Ran Benita
e48dc3c6ad Merge pull request #900 from pytest-dev/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2023-05-10 14:48:21 +03:00
pre-commit-ci[bot]
a338075c0b [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/asottile/pyupgrade: v3.3.2 → v3.4.0](https://github.com/asottile/pyupgrade/compare/v3.3.2...v3.4.0)
2023-05-09 05:16:48 +00:00
pre-commit-ci[bot]
8fbecd8a23 [pre-commit.ci] pre-commit autoupdate (#897)
updates:
- [github.com/PyCQA/autoflake: v2.1.0 → v2.1.1](https://github.com/PyCQA/autoflake/compare/v2.1.0...v2.1.1)
- [github.com/asottile/pyupgrade: v3.3.1 → v3.3.2](https://github.com/asottile/pyupgrade/compare/v3.3.1...v3.3.2)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-04-25 07:37:20 -03:00
pre-commit-ci[bot]
6317cc32fa [pre-commit.ci] pre-commit autoupdate (#896)
updates:
- [github.com/PyCQA/autoflake: v2.0.2 → v2.1.0](https://github.com/PyCQA/autoflake/compare/v2.0.2...v2.1.0)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-04-21 15:15:53 -03:00
pre-commit-ci[bot]
4bed067b06 [pre-commit.ci] pre-commit autoupdate (#895)
updates:
- [github.com/pre-commit/mirrors-mypy: v1.1.1 → v1.2.0](https://github.com/pre-commit/mirrors-mypy/compare/v1.1.1...v1.2.0)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-04-11 07:55:12 -03:00
Robert Schweizer
982ef0097a Improve docs of maxschedchunk (#893) 2023-04-06 09:46:34 -03:00
pre-commit-ci[bot]
c7d86097e9 [pre-commit.ci] pre-commit autoupdate (#892)
updates:
- [github.com/psf/black: 23.1.0 → 23.3.0](https://github.com/psf/black/compare/23.1.0...23.3.0)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-04-04 09:03:28 -03:00
pre-commit-ci[bot]
6719dfd6f8 [pre-commit.ci] pre-commit autoupdate (#889)
updates:
- [github.com/PyCQA/autoflake: v2.0.1 → v2.0.2](https://github.com/PyCQA/autoflake/compare/v2.0.1...v2.0.2)
- [github.com/pre-commit/mirrors-mypy: v1.0.1 → v1.1.1](https://github.com/pre-commit/mirrors-mypy/compare/v1.0.1...v1.1.1)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-03-14 07:52:23 -03:00
Aleksandr Mezin
6e7bbfae37 Merge pull request #888 from amezin/release-3.2.1
Release 3.2.1
2023-03-12 17:11:47 +02:00
Aleksandr Mezin
b591be4868 Release 3.2.1
Some checks failed
deploy / deploy (push) Failing after 1s
2023-03-12 14:42:09 +02:00
Aleksandr Mezin
b6c195a56c Merge pull request #885 from amezin/steal-hang-fix
Fix hang caused by `steal` command with empty test queue
2023-03-10 13:12:30 +02:00
Aleksandr Mezin
6abcdfc22e Fix hang caused by steal command with empty test queue
Fixes #884
2023-03-09 16:44:34 +02:00
pre-commit-ci[bot]
58fd7ccc05 [pre-commit.ci] pre-commit autoupdate (#881)
updates:
- [github.com/pre-commit/mirrors-mypy: v1.0.0 → v1.0.1](https://github.com/pre-commit/mirrors-mypy/compare/v1.0.0...v1.0.1)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-02-27 08:53:38 -03:00
Bruno Oliveira
ba526fad5a Merge pull request #879 from pytest-dev/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2023-02-14 07:29:00 -03:00
pre-commit-ci[bot]
efe674b265 [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/pre-commit/mirrors-mypy: v0.991 → v1.0.0](https://github.com/pre-commit/mirrors-mypy/compare/v0.991...v1.0.0)
2023-02-14 03:23:39 +00:00
Bruno Oliveira
5d692a7d63 Merge pull request #878 from akx/patch-1
docs: Remove unused statement in one-log-per-worker example
2023-02-13 11:01:58 -03:00
Aarni Koskela
5e795d88e7 docs: Remove unused statement in one-log-per-worker example 2023-02-13 15:38:59 +02:00
Aleksandr Mezin
2329d3454f Merge pull request #875 from pytest-dev/release-3.2.0
Release 3.2.0
2023-02-07 17:08:32 +02:00
Aleksandr Mezin
5c065198e9 Release 3.2.0
Some checks failed
deploy / deploy (push) Failing after 2s
Fixes #874
2023-02-07 16:46:47 +02:00
pre-commit-ci[bot]
c695763e92 [pre-commit.ci] pre-commit autoupdate (#869)
* [pre-commit.ci] pre-commit autoupdate

updates:
- [github.com/PyCQA/autoflake: v2.0.0 → v2.0.1](https://github.com/PyCQA/autoflake/compare/v2.0.0...v2.0.1)
- [github.com/psf/black: 22.12.0 → 23.1.0](https://github.com/psf/black/compare/22.12.0...23.1.0)
- [github.com/asottile/blacken-docs: v1.12.1 → 1.13.0](https://github.com/asottile/blacken-docs/compare/v1.12.1...1.13.0)

* Update .pre-commit-config.yaml

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Bruno Oliveira <nicoddemus@gmail.com>
2023-02-07 07:58:55 -03:00
Bruno Oliveira
9373ddb3f8 Merge pull request #870 from hroncok/delenv
Tests: Unset PYTEST_XDIST_AUTO_NUM_WORKERS when the behavior without the envvar is asserted
2023-01-19 20:01:41 -03:00
Miro Hrončok
8fd1bfd182 Tests: Unset PYTEST_XDIST_AUTO_NUM_WORKERS when the behavior without the envvar is asserted 2023-01-17 20:10:12 +01:00
Thomas Kolar
017cc72b70 Document limitations for debugging as well as a workaround (#867)
Closes #863
2023-01-12 12:57:20 -03:00
Pat Thiel
cf19f76d86 Fix minor typos in the documentation (#866)
Some minor typo fixes.
2023-01-12 07:52:07 -03:00
Bruno Oliveira
691a0751bf Merge pull request #865 from amezin/fix-loadsched-tests
Fix some LoadScheduling tests
2023-01-11 20:34:27 -03:00
Aleksandr Mezin
e9860923bf Fix some LoadScheduling tests
The expected number of nodes didn't match throughout the test code.
2023-01-11 22:34:56 +02:00
Aleksandr Mezin
d1dfad3e92 Implement work-stealing scheduler (#862)
Closes #858
2023-01-11 08:38:57 -03:00
Aleksandr Mezin
9b0b5b1495 Add --maxschedchunk CLI option (#857)
Maximum number of tests scheduled in one step.

Setting it to 1 will force pytest to send tests to workers one by one -
might be useful for a small number of slow tests.

Larger numbers will allow the scheduler to submit consecutive chunks of tests
to workers - allows reusing fixtures.

Unlimited if not set.

Fixes #855
Fixes #255
2022-12-23 08:21:20 -03:00
pre-commit-ci[bot]
7faa69a04b [pre-commit.ci] pre-commit autoupdate (#856)
updates:
- [github.com/psf/black: 22.10.0 → 22.12.0](https://github.com/psf/black/compare/22.10.0...22.12.0)
- [github.com/asottile/pyupgrade: v3.3.0 → v3.3.1](https://github.com/asottile/pyupgrade/compare/v3.3.0...v3.3.1)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2022-12-13 08:14:04 -03:00
Ronny Pfannschmidt
7a7be87453 Merge pull request #854 from pytest-dev/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2022-12-06 10:47:31 +01:00
pre-commit-ci[bot]
d7877cef86 [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/asottile/pyupgrade: v3.2.2 → v3.3.0](https://github.com/asottile/pyupgrade/compare/v3.2.2...v3.3.0)
2022-12-06 00:37:27 +00:00
Bruno Oliveira
639e0868f7 Merge pull request #852 from nicoddemus/release-3.1.0
Release 3.1.0
2022-12-03 11:33:06 -03:00
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
Bruno Oliveira
6d74bab518 Merge remote-tracking branch 'upstream/master' into release-1.25.0 2018-12-12 22:28:53 -02:00
Daniel Hahler
c306287a4e Merge pull request #393 from nicoddemus/fix-master
Fix test_remote_usage_prog which started to break with pytest@features
2018-12-12 23:15:45 +01:00
Bruno Oliveira
2b65800738 Fix test_remote_usage_prog which started to break with pytest@features 2018-12-12 18:25:04 -02:00
Bruno Oliveira
86bf3cb8b3 Release 1.25.0 2018-12-12 15:24:02 -02:00
Bruno Oliveira
95aaaec6c5 Merge pull request #388 from blueyed/pass-mainargv
Pass and use original sys.argv to/with workers
2018-12-12 15:21:56 -02:00
Bruno Oliveira
33d5441258 Merge pull request #390 from nicoddemus/pytest-logwarning
Prepare for the removal of pytest_logwarning in pytest 4.1
2018-12-12 08:59:21 -02:00
Bruno Oliveira
b5c09f8e84 Add CHANGELOG 2018-12-11 18:49:40 -02:00
Bruno Oliveira
a30124e858 Prepare for the removal of pytest_logwarning in pytest 4.1 2018-12-11 18:46:30 -02:00
Bruno Oliveira
c5d8635294 Merge pull request #375 from ethanhs/silencenodes
Don't show node setup when running quiet
2018-12-11 18:08:42 -02:00
Bruno Oliveira
5f0abe3cb8 Merge remote-tracking branch 'upstream/master' into ethanhs/silencenodes 2018-12-11 14:35:25 -02:00
Bruno Oliveira
3332c99e1e Improve verbosity test 2018-12-11 14:35:16 -02:00
Bruno Oliveira
3329bbbae2 Merge pull request #389 from nicoddemus/excinfo-fix-384
Fix ExceptionInfo usage for pytest 4.1
2018-12-11 14:23:21 -02:00
Bruno Oliveira
dd63d0b667 Merge pull request #386 from blueyed/minor
minor: fix error message and wrongly joined/blackified strings
2018-12-11 12:21:56 -02:00
Bruno Oliveira
dd6eac12a4 Fix ExceptionInfo usage for pytest 4.1
Fix #384
2018-12-11 12:19:34 -02:00
Daniel Hahler
a70dace69c Pass and use original sys.argv to/with workers
This gets used e.g. by argparse for the "prog" part.

We could explicitly pass it through and/or set it on
config._parser.prog, but that is a bit tedious just for this use case,
and it looks like "simulating" the main prog here appears to not be that
bad of a hack after all.
2018-12-11 06:41:18 +01:00
Daniel Hahler
6a21a5aa81 minor: fix error message and wrongly joined/blackified strings 2018-12-05 19:46:14 +01:00
Ethan Smith
49c09117ed Fix lint 2018-11-29 00:55:33 -08:00
Ethan Smith
566480fdd3 Add test and silence completely 2018-11-29 00:25:30 -08:00
bwilson
ab5f12f92e Revert "Adding RemoteTarget class to wrap module/function"
This reverts commit 311554324a.
2018-11-26 09:49:11 -06:00
bwilson
b28c3d98c5 Revert "linting"
This reverts commit b94dcfe4cd.
2018-11-26 09:49:10 -06:00
Ronny Pfannschmidt
5ecbf25450 Merge pull request #380 from nicoddemus/fix-master
Skip test which needs pytest_namespace(), removed recently
2018-11-22 07:11:19 +01:00
Ronny Pfannschmidt
646eb868ab Merge pull request #378 from tzickel/geventfix
fix a bug with gevent monkeypatch
2018-11-22 07:09:38 +01:00
Bruno Oliveira
50b27d2735 Skip test which needs pytest_namespace(), removed recently 2018-11-21 19:57:24 -02:00
tzickel
16db6eddd7 fix a bug with gevent monkeypatch 2018-11-21 18:59:28 +02:00
Ronny Pfannschmidt
f2061875e1 Merge pull request #377 from nicoddemus/pytest-4-fix
Update test that used deprecated cached_setup function
2018-11-16 19:35:04 +01:00
Bruno Oliveira
c4b365b082 Update test that used deprecated cached_setup function 2018-11-16 12:50:15 -02:00
bwilson
b94dcfe4cd linting 2018-11-13 10:45:40 -06:00
bwilson
311554324a Adding RemoteTarget class to wrap module/function 2018-11-13 10:37:37 -06:00
Ethan Smith
5465d936f7 Add newline 2018-11-12 14:05:07 -08:00
Ethan Smith
b819657dac Add changelog entry 2018-11-12 13:37:10 -08:00
Ethan Smith
d8c6c22989 Don't show node setup when running quiet 2018-11-12 13:31:19 -08:00
bwilson
5bb35b0f26 fixing linting problem with changelog 2018-11-12 13:53:35 -06:00
bwilson
47d271ad97 Adding news file 2018-11-12 13:41:18 -06:00
bwilson
7fb7bd9d04 Add a hook to allow overriding the remote module that's executed by execnet 2018-11-12 13:29:39 -06:00
Bruno Oliveira
1efec5a153 Merge pull request #372 from nicoddemus/drop-old-pytest
Drop support to old pytest versions and test with python 3.7
2018-11-11 12:15:34 -02:00
Bruno Oliveira
f9a13fe53b Drop support to old pytest versions and test with python 3.7 2018-11-11 11:27:30 -02:00
Bruno Oliveira
74a5e0ca03 Merge pull request #371 from nicoddemus/skip-module-370
Fix handling of skipped modules with pytest.skip at module level
2018-11-10 19:19:50 -02:00
Bruno Oliveira
3292e0ede0 Fix handling of skipped modules with pytest.skip at module level
Fix #332
2018-11-10 18:20:13 -02:00
Bruno Oliveira
d40d47acd9 Merge pull request #369 from nicoddemus/release-1.21.1
Release 1.21.1
2018-11-10 11:51:32 -02:00
Bruno Oliveira
3579bfce7d Release 1.21.1 2018-11-09 14:29:21 -02:00
Ronny Pfannschmidt
365499eeaf Merge pull request #368 from nicoddemus/rename-filescope-file
Rename filescope to loadfile
2018-11-09 17:20:35 +01:00
Ronny Pfannschmidt
a518bd4e2a Merge pull request #362 from nicoddemus/serialize-fail-349
Handle warnings with unserializable arguments
2018-11-09 17:17:59 +01:00
Bruno Oliveira
d84ae73775 Rename filescope to loadfile
This makes it consistent with the option (--dist=loadfile) and the other
scheduling file (loadscope.py)
2018-11-09 13:00:40 -02:00
Bruno Oliveira
673fe9bd67 Create 'baseline' stage for fast-failure of builds, similar to pytest 2018-10-30 09:44:13 -03:00
Bruno Oliveira
9217b80fb1 Update CI to test against latest pytest release 2018-10-30 09:44:13 -03:00
Bruno Oliveira
b151e56fbd Handle unserializable warning arguments
Fix #349
2018-10-30 09:44:13 -03:00
Bruno Oliveira
70688b7986 Refactor warning acceptance tests into a class 2018-10-29 18:38:01 -03:00
Bruno Oliveira
adc385253c Merge pull request #357 from nicoddemus/release-1.24.0
Release 1.24.0
2018-10-29 18:23:37 -03:00
Bruno Oliveira
bfd6029102 Release 1.24.0 2018-10-18 07:55:54 -03:00
Bruno Oliveira
3f5a8eeb7f Merge pull request #356 from pristupa/maxprocessing-option
Add --maxprocesses option to use with --numprocesses=auto
2018-10-18 07:54:02 -03:00
Bruno Oliveira
d5b61e72d4 Merge pull request #351 from cybojanek/master
Issue node shutdown on empty workload
2018-10-18 07:53:52 -03:00
Bruno Oliveira
f224c4b00e Fix CHANGELOG formatting 2018-10-17 09:19:48 -03:00
Pavel Pristupa
ddefd3f453 Call group.addoption() instead of _addoption() when parsing command-line arguments
Fix #337
2018-10-17 17:48:47 +07:00
Pavel Pristupa
5ce8a6ec1a Add maxprocesses option to limit the maximum number of workers when using numprocesses=auto
Fix #337
2018-10-17 17:08:56 +07:00
Jan Kasiak
fe06f27b8b Issue node shutdown on empty workload
Remote pytest_runtestloop requires at least 2 tests, or a test
and a shutdown command. In case of inter test locking, for
example using a file lock, the tests could deadlock, since the
shutdown command would not be enqueued, and the last test of a
worker would never finish, and would not allow another worker
to make progress.
2018-09-30 15:39:04 -04:00
Bruno Oliveira
86e2fb5a5a Merge pull request #347 from nicoddemus/release-1.23.2
Release 1.23.2
2018-09-28 07:34:43 -03:00
Bruno Oliveira
ec6129639e Release 1.23.2 2018-09-28 06:38:50 -03:00
Bruno Oliveira
f6ac209cd5 Merge pull request #345 from BrandonHoffman/master
add exception handeling for warning serialization issues
2018-09-28 06:38:14 -03:00
Bruno Oliveira
b176b37606 Add a test which reproduces #344 and fix the handling code
Fix #344
2018-09-27 22:18:52 -03:00
Brandon Hoffman
22e36c9cb4 run black formatter 2018-09-27 19:32:10 -05:00
Brandon Hoffman
8825e7e234 use module name and class name for method instead of the reference to them 2018-09-27 19:27:49 -05:00
Brandon Hoffman
5d5f87b600 add moduel and class name to warning text 2018-09-27 19:24:58 -05:00
Brandon Hoffman
571d08fb8b remove unneeded lines 2018-09-26 21:05:30 -05:00
Brandon Hoffman
36c28ee154 add test case for invalid warning handeling 2018-09-26 20:44:01 -05:00
Brandon Hoffman
af3e975664 add changelog 2018-09-26 19:46:28 -05:00
Brandon Hoffman
bc2ba55f70 add exception handeling for warning serialization issues 2018-09-26 19:32:13 -05:00
Bruno Oliveira
8475876ce3 Merge pull request #343 from nicoddemus/release-1.23.1
Release 1.23.1
2018-09-25 17:05:40 -03:00
Bruno Oliveira
8ee6c0a4e6 Release 1.23.1 2018-09-25 12:11:08 -03:00
Bruno Oliveira
ec966d9800 Merge pull request #342 from nicoddemus/3.8-warnings
Fix warnings transfer between workers and master node with pytest >= 3.8
2018-09-25 12:10:10 -03:00
Bruno Oliveira
78030e3d32 Fix warnings transfer between workers and master node with pytest >= 3.8
Fix #341
2018-09-25 10:01:20 -03:00
Bruno Oliveira
ed2ab76b5c Merge pull request #334 from nicoddemus/release-1.23.0
Release 1.23.0
2018-08-25 01:49:04 -03:00
Bruno Oliveira
bea2467712 Release 1.23.0 2018-08-23 16:33:06 -03:00
Bruno Oliveira
c4a793de38 Merge pull request #332 from nicoddemus/optimize-report-data
Optimize collection performance
2018-08-23 16:31:09 -03:00
Bruno Oliveira
9bcd0e4352 Optimize collection performance
Only send failed collection reports, as master is only interested in failed
reports.

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

Fix #305
2018-07-23 17:36:03 -03:00
Bruno Oliveira
55dc257f1c Merge pull request #314 from nicoddemus/skip-tags-appveyor
Skip AppVeyor when building on tags
2018-07-23 17:14:14 -03:00
Bruno Oliveira
46dbc1223b Merge pull request #313 from nicoddemus/conda-forge-badge
Replace anaconda's badge by conda-forge's
2018-07-23 17:14:03 -03:00
Bruno Oliveira
9b46df48a6 Merge pull request #312 from nicoddemus/release-1.22.3
Release 1.22.3
2018-07-23 17:13:26 -03:00
Bruno Oliveira
c6306e4fbe Skip AppVeyor when building on tags 2018-07-23 17:03:06 -03:00
Bruno Oliveira
6ec967a3f2 Prepare release 1.22.3 2018-07-23 16:36:39 -03:00
Bruno Oliveira
0722ea12ef Use a tox environment to update the release files 2018-07-23 16:36:26 -03:00
Bruno Oliveira
a5d51f24c9 Replace anaconda's badge by conda-forge's 2018-07-23 16:33:05 -03:00
Ronny Pfannschmidt
bb91f5459d Merge pull request #309 from greut/cpu-count
plugin: get cpu_count from sched_getaffinity
2018-07-23 20:13:45 +02:00
Bruno Oliveira
00b7c4553a Merge pull request #311 from nicoddemus/skip-deploy
Skip deploy job on Travis if we are not on a tag
2018-07-21 19:20:25 -03:00
Bruno Oliveira
9ff6fc10d0 Skip deploy job on Travis if we are not on a tag 2018-07-21 18:17:21 -03:00
Bruno Oliveira
a06e75cc78 Fix flake8 issue 2018-07-21 12:09:44 -03:00
Yoan Blanc
04882074e1 plugin: get cpu_count from sched_getaffinity
Signed-off-by: Yoan Blanc <yoan.blanc@exoscale.ch>
2018-07-18 08:32:01 +02:00
Bruno Oliveira
06593498cd Merge pull request #306 from asottile/factor_out_py_std
Factor out py.std
2018-07-06 22:21:56 -03:00
Bruno Oliveira
57d09c2622 Merge pull request #308 from scop/dropthedot
Use pytest instead of py.test per upstream recommendation, #dropthedot
2018-07-06 22:20:53 -03:00
Ville Skyttä
663bbd5338 Use pytest instead of py.test per upstream recommendation, #dropthedot
http://blog.pytest.org/2016/whats-new-in-pytest-30/
https://twitter.com/hashtag/dropthedot
2018-07-07 00:01:11 +03:00
Bruno Oliveira
137b939a27 Merge pull request #307 from scop/grammar
Grammar fix
2018-07-06 12:05:26 -03:00
Ville Skyttä
17f6257a24 Grammar fix 2018-07-06 15:45:41 +03:00
Anthony Sottile
78fcf725cf Factor out py.std 2018-07-05 15:35:39 -07:00
Ronny Pfannschmidt
8d94fca964 Merge pull request #297 from nicoddemus/fix-init
Fix __init__ declaration
2018-06-11 06:01:17 +02:00
Bruno Oliveira
b06c52a5d2 Add .pytest_cache to gitignore 2018-06-03 16:10:55 -03:00
Bruno Oliveira
fd2e8a6711 Make all classes subclass from object 2018-06-03 16:10:24 -03:00
Bruno Oliveira
98ae25c12c Fix __init__ declaration 2018-06-03 16:06:52 -03:00
Bruno Oliveira
182d1f6433 Merge pull request #287 from nicoddemus/release-1.22.2
Preparing 1.22.2 release
2018-02-27 07:43:11 -03:00
Bruno Oliveira
c92bbb021a Preparing 1.22.2 release 2018-02-26 18:46:11 -03:00
Bruno Oliveira
710b324902 Merge pull request #286 from nicoddemus/fix-slaveoutput
Add backward compatibility for ``slaveoutput`` attribute to ``WorkerController`` instances
2018-02-26 18:44:24 -03:00
Bruno Oliveira
76e7b8d0c0 Add backward compatibility for `slaveoutput attribute to WorkerController` instances
Fix #285
Fix pytest-dev/pytest-cov#191
2018-02-26 14:52:55 -03:00
Bruno Oliveira
63b329c251 Merge pull request #281 from nicoddemus/release-1.22.1
Release 1.22.1
2018-02-19 19:10:07 -03:00
Bruno Oliveira
de993e1ee3 Prepare changelog for 1.22.1 2018-02-19 18:10:50 -03:00
Bruno Oliveira
af4409753a Show content for trivial changelog entries 2018-02-19 18:10:10 -03:00
Bruno Oliveira
c39cd10ae5 Small improvements to HOWTORELEASE 2018-02-19 18:07:13 -03:00
Ronny Pfannschmidt
11e3599785 Merge pull request #277 from lmtierney/single-loadscope
Ensure loadscope and loadfile work when the first scope has one work unit
2018-02-19 20:02:22 +01:00
Bruno Oliveira
1be8a463ff Add docstring to tests for #277 2018-02-16 20:14:47 -02:00
lmtierney
bd3c7dba8b Ensure loadscope and loadfile work when the first scope has one work unit 2018-02-08 19:51:42 -06:00
Ronny Pfannschmidt
d91ec5503e Merge pull request #262 from nicoddemus/titles-readme
Improve README to be more newcomer friendly
2018-02-08 07:49:08 +01:00
Bruno Oliveira
23ce4502f1 Merge pull request #268 from feuillemorte/234-master-worker-terminology
#234 use master/worker terminology
2018-02-06 07:27:31 -02:00
feuillemorte
6fb01801ac #234 Fix comments 2018-02-06 11:53:45 +03:00
Bruno Oliveira
ee2a1b4f4d Add an explicit test for backward compatibility with "config.slaveinput"
Related to #234
2018-01-30 19:13:14 -02:00
feuillemorte
b04703b6ba #234 Fix flake 8 and fix comments 2018-01-17 23:34:39 +03:00
feuillemorte
9a5a81962c Merge branch '234-master-worker-terminology' of github.com:feuillemorte/pytest-xdist into 234-master-worker-terminology 2018-01-17 23:07:45 +03:00
feuillemorte
40491279ef #234 Added master/worker terminology 2018-01-17 23:07:29 +03:00
Bruno Oliveira
9efe14946a Improve changelog formatting 2018-01-16 18:26:50 -02:00
feuillemorte
708256228b Add changelog entry 2018-01-16 21:49:54 +03:00
feuillemorte
f1e6dc4344 use master/worker terminology 2018-01-16 21:46:43 +03:00
Bruno Oliveira
3098f27731 Merge pull request #267 from nicoddemus/release-1.22.0
Prepare release 1.22.0
2018-01-11 18:13:41 -02:00
Bruno Oliveira
6c68478997 Prepare release 1.22.0 2018-01-11 17:45:31 -02:00
Ronny Pfannschmidt
0181829ba7 Merge pull request #266 from nicoddemus/pytest_runtest_logfinish
Add support for the new pytest_runtest_logfinish hook (pytest 3.4)
2018-01-11 10:47:31 +01:00
Bruno Oliveira
9b767a8dcb Add CHANGELOG entry for #266 2018-01-10 17:55:36 -02:00
Bruno Oliveira
449e6c2e00 Add support for the new pytest_runtest_logfinish hook (pytest 3.4) 2018-01-10 17:52:27 -02:00
Bruno Oliveira
4dc86a1dad Merge pull request #265 from nicoddemus/overview-readme
Add reference to OVERVIEW to README
2018-01-05 22:07:34 -02:00
Bruno Oliveira
c4c4dab53f Add reference to OVERVIEW to README 2018-01-05 13:58:19 -02:00
Bruno Oliveira
17978f3bea Merge pull request #263 from nicoddemus/release-1.21.0
Prepare release 1.21.0
2017-12-26 11:00:29 -02:00
Bruno Oliveira
9b2163f028 Update CHANGELOG with towncrier 2017-12-22 17:34:12 -02:00
Bruno Oliveira
e72eadb413 Fix linting 2017-12-22 17:30:47 -02:00
Bruno Oliveira
6cd1eb2f43 Use more modern python and reword part about pytest ini configuration 2017-12-22 16:53:18 -02:00
Bruno Oliveira
7c3a86a549 Mention pytest-forked instead of the help for --boxed 2017-12-22 16:49:14 -02:00
Bruno Oliveira
b2c63a1ba5 Reworded some sections to be more newbie friendly 2017-12-22 16:42:35 -02:00
Bruno Oliveira
3f545ee621 Change section titles so they are more prominent in the README 2017-12-22 16:29:10 -02:00
Bruno Oliveira
3f09e158be Merge pull request #242 from Corni/loadfilescope
Load-Distribute test cases by filename
2017-12-22 16:27:20 -02:00
Bruno Oliveira
0d19e7f22b Merge remote-tracking branch 'upstream/master' into Corni/loadfilescope 2017-12-22 16:07:51 -02:00
Bruno Oliveira
aedad85e35 Add --dist=loadfile doc to README 2017-12-22 16:07:33 -02:00
Bruno Oliveira
a020c9509d Small adjustment to CHANGELOG 2017-12-22 16:07:26 -02:00
Ronny Pfannschmidt
efce50fe6a Merge pull request #261 from nicoddemus/pytest33
Fix tests in pytest >= 3.3 and add it to build matrix
2017-12-22 18:51:02 +01:00
Bruno Oliveira
db1fb683cf Add pytest 3.3 to tox build matrix 2017-12-22 15:38:07 -02:00
Bruno Oliveira
28d60f94f3 Fix load scope tests in pytest 3.3
Fix #260
2017-12-22 15:35:24 -02:00
Bruno Oliveira
ed9f25a0d2 Merge pull request #259 from hugovk/rm-eol
Drop support for EOL Python 2.6
2017-12-22 15:24:07 -02:00
Hugo
cb8b161d36 Add towncrier changelog entry 2017-12-21 11:31:38 +02:00
Hugo
cec33cca8a Add Python versions badge and alts to other badges 2017-12-21 11:26:59 +02:00
Hugo
c6f2fdcc31 Remove redundant parentheses 2017-12-21 11:19:58 +02:00
Hugo
8b79d909ea Rewrite unnecessary list literals as set literals 2017-12-21 11:04:19 +02:00
Hugo
2b0dc3f83a Remove redundant Python 3.2 code 2017-12-21 10:59:52 +02:00
Hugo
ed2db3d03a Drop support for EOL Python 2.6 2017-12-21 10:55:57 +02:00
Hugo
d3229af3da Ignore IDE metadata 2017-12-21 10:43:24 +02:00
Cornelius Riemenschneider
fad312fdb7 Add scheduling by test file. 2017-12-14 16:18:46 +01:00
Cornelius Riemenschneider
d4f977f3b7 Add PyCharm files to .gitignore. 2017-12-14 16:08:40 +01:00
Ronny Pfannschmidt
5889e4c229 Merge pull request #248 from timyhou/issue_241
Issue #241 Make copy during serialization of reprtraceback and reprcr…
2017-11-28 14:59:08 +01:00
Bruno Oliveira
239065810e Merge remote-tracking branch 'upstream/master' into timyhou/issue_241 2017-11-22 21:29:32 -02:00
Bruno Oliveira
bf0645ad35 Merge pull request #249 from nicoddemus/flakes
Fix E741 and E722 flake errors
2017-11-22 21:28:52 -02:00
Bruno Oliveira
cf067a86d7 Update pip on AppVeyor
py26-pytest30 is failing due to a setuptools error. Try to update
pip to see if this fixes the issue.
2017-11-22 20:58:06 -02:00
Bruno Oliveira
58f7291a99 Fix E741 and E722 flake errors 2017-11-22 20:00:39 -02:00
thou
2eb1002781 Issue #241 Make copy during serialization of reprtraceback and reprcrash to avoid mutating. 2017-11-21 12:33:07 -06:00
Bruno Oliveira
635d3d02cb Merge pull request #239 from nicoddemus/release-1.20.1
Release 1.20.1
2017-10-05 17:22:48 -03:00
Bruno Oliveira
adaa10f285 Release 1.20.1 2017-10-05 16:55:45 -03:00
Ronny Pfannschmidt
1cd7001647 Merge pull request #233 from blueyed/improve-pdb-usage-error
Improve UsageError when used with --pdb
2017-10-05 13:19:25 +02:00
Bruno Oliveira
95b660e26a Merge pull request #232 from verdesmarald/master
Don't create DSession in collect-only mode
2017-10-05 07:35:36 -03:00
Ronny Pfannschmidt
7a5efcdb76 Merge pull request #238 from timj/u/timj/exit-when-all-dead
Stop looping if all workers have died
2017-10-05 08:03:12 +02:00
Tim Jenness
9e59d07947 Stop looping if all workers have died
If the workers are crashing and the restart limit has been met,
we need to stop listening for events and trigger an internal
error.
2017-10-04 17:59:36 -07:00
Daniel Hahler
3799caa4fc Improve UsageError when used with --pdb
It might not be clear that pytest-xdist is used at all, and this should
help users to make --pdb work.
2017-09-23 21:07:39 +02:00
verdesmarald
1e66af6389 Fix flake8 formatting errors 2017-09-07 16:40:32 +10:00
verdesmarald
7674f6b186 Add tests for interactions between boxed, dist and collect-only.
Re-enable parsing of the boxed option with collect-only
2017-09-07 16:09:06 +10:00
verdesmarald
c83f73e19c Add news file for bugfix 2017-09-04 14:32:19 +10:00
verdesmarald
c874872190 Don't create DSession in collect-only mode 2017-09-04 14:25:42 +10:00
Ronny Pfannschmidt
e4f36b18f3 Merge pull request #228 from nicoddemus/ci-pytest
Add test environments using pytest master and features branches
2017-09-02 18:33:54 +02:00
Bruno Oliveira
0cdef59428 Add test environments using pytest master and features branches 2017-09-01 18:51:49 -03:00
Bruno Oliveira
4bb06e3f74 Merge pull request #227 from EdwardBetts/spelling
correct spelling mistake
2017-09-01 17:22:12 -03:00
Edward Betts
2c9b0b8894 correct spelling mistake 2017-09-01 20:45:18 +01:00
Bruno Oliveira
b464335c83 Fix link in CHANGELOG 2017-08-19 06:11:32 -03:00
Ronny Pfannschmidt
24b478446a Merge pull request #225 from pytest-dev/Ronny-patch-1
fix up travis deploy config
2017-08-18 15:47:25 +02:00
Ronny Pfannschmidt
d4d52dc933 fix up travis deploy config
move distributions key to the right place
install setuptools_scm for deploy
2017-08-18 14:00:44 +02:00
Bruno Oliveira
e5c2347934 Merge pull request #224 from nicoddemus/release-1.20.0
Release 1.20.0
2017-08-18 08:47:10 -03:00
Bruno Oliveira
577418f28c Prepare release 1.20.0 2017-08-17 18:34:03 -03:00
Bruno Oliveira
5c46cdf62d Mention flaky in the changelog for #206 2017-08-17 18:33:50 -03:00
Ronny Pfannschmidt
493ba6cb1d Merge pull request #223 from nicoddemus/travis-stages
Change Travis configuration to use stages in CI
2017-08-17 09:39:16 +02:00
Bruno Oliveira
d9bb158085 Change Travis configuration to use stages in CI
Fix #221
2017-08-16 23:54:38 -03:00
Ronny Pfannschmidt
0175605c57 Merge pull request #219 from nicoddemus/worker-id-docstring
Add docstring for worker_id
2017-08-11 15:49:26 +02:00
Bruno Oliveira
3ed405f22f Add docstring for worker_id
Fix #190
2017-08-10 21:51:30 -03:00
Bruno Oliveira
7b9546b89b Merge pull request #218 from nicoddemus/teardown-crash
Identify correct test crashed during teardown and support multiple test logs from plugins
2017-08-10 21:33:08 -03:00
Bruno Oliveira
542111d272 Merge pull request #217 from nicoddemus/release-1.19.1
Prepare release 1.19.1
2017-08-10 12:50:53 -03:00
Bruno Oliveira
2adf810e5e Add acceptance test for plugins which log tests multiple times
Fix #206
2017-08-10 11:18:52 -03:00
Bruno Oliveira
f8e138b986 Fix tests incorrectly identified if worker crashes during teardown stage
Fix #124
2017-08-10 10:43:40 -03:00
Bruno Oliveira
469c8c50d0 Prepare release 1.19.1 2017-08-10 07:17:59 -03:00
Ronny Pfannschmidt
79f1b6dab2 Merge pull request #215 from nicoddemus/logwarning-py-path
Fix crash when transferring internal pytest warnings from workers to the master node
2017-08-10 08:57:21 +02:00
Ronny Pfannschmidt
75e61d86b7 Merge pull request #213 from messa/patch-1
Fix reprcrash check in unserialize_report()
2017-08-10 08:55:51 +02:00
Bruno Oliveira
20a1b9ef11 Fix crash when transferring internal pytest warnings from workers to the master node
Fix #214
2017-08-09 17:55:16 -03:00
Bruno Oliveira
a54f531701 Merge pull request #212 from nicoddemus/release-1.19.0
Prepare release 1.19.0
2017-08-09 12:44:59 -03:00
Petr Messner
4389843dcb Fix reprcrash check in unserialize_report() 2017-08-09 17:44:45 +02:00
Bruno Oliveira
3bb1a8e467 Prepare release 1.19.0 2017-08-09 12:24:04 -03:00
Bruno Oliveira
9c8db61b6d Merge pull request #209 from nicoddemus/warnings-support
Warnings support
2017-08-09 11:05:49 -03:00
Bruno Oliveira
fb6a6872a0 Use pkg_resources to parse the actual pytest version 2017-08-09 10:10:01 -03:00
Bruno Oliveira
0987356f64 Increase max-line-size for flake8 to 100 and fix other flake errors 2017-08-09 10:10:01 -03:00
Bruno Oliveira
c31ec8b222 Skip test_logwarning on pytest versions older than 3.1 2017-08-09 10:10:01 -03:00
Bruno Oliveira
d0474076e9 Transfer warnings from workers to master
Fix #92
2017-08-09 10:10:01 -03:00
Bruno Oliveira
02754d84d3 Remove __multicall__ from pytest_sessionfinish 2017-08-09 10:10:01 -03:00
Bruno Oliveira
0738c3e1e2 Merge pull request #210 from pytest-dev/fix-travis
Fix travis, add more recent pytest versions
2017-08-09 09:52:52 -03:00
Ronny Pfannschmidt
15c51c497b mark looponfail tests broken by 3.1+ as xfail 2017-08-09 14:31:50 +02:00
Ronny Pfannschmidt
0515f59c63 use sane travis config add newer pytest versions and remove tox-travis 2017-08-09 14:21:19 +02:00
Bruno Oliveira
d5d630f1b7 Small grammar fix in README 2017-08-09 07:02:41 -03:00
Ronny Pfannschmidt
e3eae96bb3 Merge pull request #208 from nicoddemus/docs-distscope
Add documentation for new ``--dist=loadscope``
2017-08-09 07:25:00 +02:00
Bruno Oliveira
bc54b5b6b1 Add documentation for new `--dist=loadscope` 2017-08-08 18:52:02 -03:00
Ronny Pfannschmidt
9884285915 Merge pull request #202 from nicoddemus/traceback-error
Fix serialization of native tracebacks
2017-08-08 12:50:29 +02:00
Bruno Oliveira
d564364c44 Add acceptance test for different traceback styles 2017-08-07 20:22:21 -03:00
Bruno Oliveira
435e5e8ac5 Fix serialization report tests
No idea why using "-n1" on one of the those tests makes the
second test to fail.

See pytest-dev/pytest-xdist#202 for a full description.
2017-08-07 20:21:40 -03:00
Bruno Oliveira
fbe793ed04 Merge pull request #1 from RonnyPfannschmidt/remove-boxed
Remove boxed and provide --boxed via pytest-forked
2017-08-05 05:06:24 -03:00
Ronny Pfannschmidt
c454d410a1 fix version number 2017-08-05 09:33:10 +02:00
Ronny Pfannschmidt
f856cb74f4 fix linting issue 2017-08-05 08:28:30 +02:00
Bruno Oliveira
5dc87143ad Fix serialization of native tracebacks
Fix #196
2017-08-04 20:13:30 -03:00
Ronny Pfannschmidt
060e7b85ad include brunos review suggestions for changes 2017-08-04 23:24:09 +02:00
Ronny Pfannschmidt
1d631ca1cb Merge pull request #201 from nicoddemus/non-determinist-tests
Make tests for loadscope deterministic
2017-08-04 23:21:34 +02:00
Bruno Oliveira
d6139b2525 Make tests for loadscope deterministic
Fix #199
2017-08-04 18:18:42 -03:00
Ronny Pfannschmidt
7f20416a70 changelog entry 2017-08-04 21:32:44 +02:00
Ronny Pfannschmidt
5db31f1ee7 add support for --boxed by aliasing to --forked of pytest-foked 2017-08-04 21:31:01 +02:00
Ronny Pfannschmidt
56d62c2d37 remove boxed from readme 2017-08-04 21:01:36 +02:00
Ronny Pfannschmidt
3f96e1b97f remove boxed from example -n1 is sufficient and quicker to run 2017-08-04 21:01:36 +02:00
Ronny Pfannschmidt
ad3e45590f remove the boxed plugin code 2017-08-04 21:01:36 +02:00
Ronny Pfannschmidt
2c66187dd8 Merge pull request #191 from HPENetworking/loadsuite
New scheduler for distribution of groups of related tests
2017-08-03 10:59:53 +02:00
Bruno Oliveira
6e99c0d4c5 Fix flakes 2017-08-02 21:33:58 -03:00
Bruno Oliveira
a7f8c22ae5 Make "-nX" and "--dist=loadscope" work properly together 2017-08-02 21:23:52 -03:00
Bruno Oliveira
43f7dbeb02 Add functional tests for --dist=loadscope 2017-08-02 21:05:18 -03:00
Bruno Oliveira
208f1f1f72 Merge pull request #195 from nicoddemus/release-1.18.2
Prepare CHANGELOG for 1.18.2
2017-08-02 18:23:10 -03:00
Carlos Jenkins
94fc4fbb85 Added an example of a test suite that run with the loadscope scheduler. 2017-07-28 23:55:09 -06:00
Carlos Jenkins
b0916f06ce Implemented a load scheduler that groups tests by scope. 2017-07-28 23:55:09 -06:00
Carlos Jenkins
8356cfa39d Split schedulers into their own submodule. 2017-07-28 23:55:09 -06:00
Bruno Oliveira
f27265cd28 Prepare CHANGELOG for 1.18.2 2017-07-28 07:31:45 -03:00
Bruno Oliveira
6b0a8c633b Fix changelog grammar 2017-07-28 07:31:27 -03:00
Ronny Pfannschmidt
7f2ca0aecb Merge pull request #194 from nicoddemus/error-handler
Fix internal handling in process_from_remote
2017-07-28 07:33:56 +02:00
Bruno Oliveira
64f8ad5469 Fix internal handling in process_from_remote
Fix #175
2017-07-27 20:50:09 -03:00
Ronny Pfannschmidt
144b37ae4b Merge pull request #192 from pytest-dev/update-readme
Update README.rst
2017-07-24 20:15:18 +02:00
Florian Bruhin
e41ba03edb Update README.rst
Remove easy_install because (hopefully) nobody uses that anymore nowadays, and remove the outdated note about using the pytest issue tracker.
2017-07-24 20:04:58 +02:00
Bruno Oliveira
cb3e734397 Add towncrier link to PULL_REQUEST_TEMPLATE.md 2017-07-17 17:56:58 -03:00
Ronny Pfannschmidt
af8a5742ad Merge pull request #186 from jayvdb/patch-1
Correction of example news file extension
2017-07-16 10:02:14 +02:00
Ronny Pfannschmidt
d2d5ae9f5f Merge pull request #185 from jayvdb/remove-implicit-dep
Removal of unnecessary dependency on py
2017-07-16 10:01:29 +02:00
John Vandenberg
05932011ed Correction of example news file extension 2017-07-14 19:48:56 +07:00
John Vandenberg
cd12d174c4 Removal of unnecessary dependency on py
`pytest-xdist` depends on `pytest>=3`.
`pytest 3.0.0` requires `py>=1.4.29`
and latest pytest requires `py>=1.4.33`.
Thus it is unnecessary for pytest-xdist to include
a requirement on `py>=1.4.22`

Fixes https://github.com/pytest-dev/pytest-xdist/issues/105
2017-07-14 19:40:40 +07:00
Bruno Oliveira
3652e21f3b Prepare release 1.18.1 2017-07-05 21:25:10 -03:00
Bruno Oliveira
a84b106a3b Add issue links to changelog template 2017-07-05 21:17:16 -03:00
Bruno Oliveira
5d757f9db3 Merge pull request #178 from timyhou/locals_d_issue
Unserialize reprlocals to a ReprLocal instance
2017-07-05 12:46:41 -03:00
Bruno Oliveira
674c623ffa Merge pull request #180 from luisincrespo/patch-1
Update CHANGELOG.rst
2017-07-05 12:36:00 -03:00
Luis Crespo
1fca8ee16d Update CHANGELOG.rst 2017-07-05 11:17:54 -04:00
Luis Crespo
185595f596 Update CHANGELOG.rst 2017-07-05 11:17:36 -04:00
Luis Crespo
55bc8c49d6 Update CHANGELOG.rst
Update `1.18.0` entry in CHANGELOG to reflect that `pytest-xdist` now requires `pytest>=3.0.0`.
2017-07-05 11:14:52 -04:00
thou
530c9b234a Unserialize reprlocals to a ReprLocal instance 2017-06-30 15:00:50 -05:00
Bruno Oliveira
1ca9eff35d Fixed changelog wording to be more user friendly 2017-06-28 18:32:59 -03:00
Bruno Oliveira
5320d25e47 Merge pull request #171 from timyhou/longrepr.sections_issue
Serialization/Deserialization was dropping longrepr.sections
2017-06-28 18:31:19 -03:00
thou
96ad4d1cfd Added news fragment 2017-06-28 10:58:37 -05:00
thou
5c1a35fee4 Serialization / Deserialization was dropping longrepr.sections 2017-06-27 16:30:49 -05:00
Bruno Oliveira
665b8eda8d Merge pull request #170 from nicoddemus/release-1.18.0
release 1.18.0
2017-06-26 21:28:30 -03:00
Bruno Oliveira
cee78e9b20 CHANGELOG for 1.18.0 2017-06-26 15:16:48 -03:00
Bruno Oliveira
6e1154881d Merge pull request #164 from timyhou/master
Issue #130
2017-06-23 19:39:27 -03:00
Ronny Pfannschmidt
a96f6c9038 Merge pull request #168 from kain88-de/numprocesses-option
add '--numprocesses' as long option for '-n'
2017-06-23 09:45:46 +02:00
Bruno Oliveira
47b65716a3 Fix test for --numprocess flag 2017-06-22 21:47:34 -03:00
Max Linke
dbaf49cd69 add '--numprocesses' as long option for '-n'
This is more verbose in scripts that run pytest-xdist. It can also help
in some ci-helper scripts like to auto detect if  pytest-xdist should be
installed.
2017-06-22 21:53:16 +02:00
Bruno Oliveira
41c00ca47e Clarify PR template
Clarify that a new "file" should be added to "changelog" folder.
2017-06-22 16:41:27 -03:00
thou
f0dee10a0d Fix failing test since spawn.out does not contain "LOOPONFAILING" in pytest >= 3 2017-06-21 13:37:03 -05:00
Tim Hou
6256608669 Update tox and appveyor for CI tests to exclude pytest < 3
Update the install requires versions for pytest
2017-06-21 13:37:02 -05:00
thou
4a1f1d879b Updated serialize_report and unserialize_report to pass entire exception info using ReprExceptionInfo.
This addresses issue #130.
2017-06-21 13:36:58 -05:00
Ronny Pfannschmidt
2094157950 Merge pull request #162 from nicoddemus/let-setuptools-scm-manage-manifest
Let setuptools_scm manage the files that go into the package
2017-06-14 13:43:46 +02:00
Bruno Oliveira
7473683cdb Let setuptools_scm manage the files that go into the package
As discussed in #161, setuptools_scm will automatically
add all version controlled files to the package, so
MANIFEST.in is no longer needed

Fix #161
2017-06-14 07:58:57 -03:00
Bruno Oliveira
c48834aa69 Merge pull request #159 from nicoddemus/release-1.17.1
Release 1.17.1
2017-06-10 10:59:42 -03:00
Bruno Oliveira
6d9d6c6a34 Update HOWTORELEASE docs 2017-06-10 10:03:10 -03:00
Bruno Oliveira
1542aff326 Update CHANGELOG 2017-06-10 10:02:55 -03:00
Bruno Oliveira
cc95cf26e1 Skip uploading docs when publishing to PyPI
This should also solve the problem of not publishing wheels
2017-06-10 10:02:43 -03:00
Bruno Oliveira
eb90a7c82c Merge pull request #158 from nicoddemus/revert-pr-125
Revert PR 125 due to 1.17.0 breakage
2017-06-10 09:55:51 -03:00
Bruno Oliveira
afe8bad9ff Add news fragment
Fix #157
2017-06-10 09:44:55 -03:00
Bruno Oliveira
95a62460aa Revert "Mark test as complete at teardown."
This reverts commit 82b992a5fd.
2017-06-10 09:43:59 -03:00
Bruno Oliveira
8e2fdeee3e Revert "- Reinforce test_crashing_item to match test name instead of test"
This reverts commit 99f667380f.
2017-06-10 09:43:59 -03:00
Bruno Oliveira
fae1e02c1d Revert "Fix missing blank line."
This reverts commit 602694b817.
2017-06-10 09:43:59 -03:00
Ronny Pfannschmidt
3440f08a40 Merge pull request #156 from nicoddemus/release-howto-towncrier
Add towncrier and HOWTORELEASE docs
2017-06-10 09:00:47 +02:00
Bruno Oliveira
a9b064d992 Add HOWTORELEASE document
Fix #155
2017-06-09 19:50:54 -03:00
Bruno Oliveira
47875cbea9 Add towncrier for CHANGELOG management
Fix #154
2017-06-09 19:49:06 -03:00
78 changed files with 10019 additions and 3871 deletions

15
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View File

@@ -0,0 +1,15 @@
Thanks for submitting a PR, your contribution is really appreciated!
Here's a quick checklist that should be present in PRs:
- [ ] Make sure to include reasonable tests for your change if necessary
- [ ] We use [towncrier](https://pypi.python.org/pypi/towncrier) for changelog management, so please add a *news* file into the `changelog` folder following these guidelines:
* Name it `$issue_id.$type` for example `588.bugfix`;
* If you don't have an issue_id change it to the PR id after creating it
* Ensure type is one of `removal`, `feature`, `bugfix`, `vendor`, `doc` or `trivial`
* Make sure to use full sentences with correct case and punctuation, for example:
```
Fix issue with non-ascii contents in doctest text files.
```

13
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,13 @@
# Keep GitHub Actions up to date with GitHub's Dependabot...
# https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot
# https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#package-ecosystem
version: 2
updates:
- package-ecosystem: github-actions
directory: /
groups:
github-actions:
patterns:
- "*" # Group all Actions updates into a single larger pull request
schedule:
interval: weekly

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

@@ -0,0 +1,57 @@
name: deploy
on:
workflow_dispatch:
inputs:
version:
description: 'Release version'
required: true
default: '1.2.3'
jobs:
package:
runs-on: ubuntu-latest
env:
SETUPTOOLS_SCM_PRETEND_VERSION: ${{ github.event.inputs.version }}
steps:
- uses: actions/checkout@v4
- name: Build and Check Package
uses: hynek/build-and-inspect-python-package@v2.13
deploy:
needs: package
runs-on: ubuntu-latest
environment: deploy
permissions:
id-token: write # For PyPI trusted publishers.
contents: write # For tag.
steps:
- uses: actions/checkout@v4
- name: Download Package
uses: actions/download-artifact@v4
with:
name: Packages
path: dist
- name: Publish package to PyPI
uses: pypa/gh-action-pypi-publish@v1.12.4
with:
attestations: true
- name: Push tag
run: |
git config user.name "pytest bot"
git config user.email "pytestbot@gmail.com"
git tag --annotate --message=v${{ github.event.inputs.version }} v${{ github.event.inputs.version }} ${{ github.sha }}
git push origin v${{ github.event.inputs.version }}
- name: GitHub Release
uses: softprops/action-gh-release@v2
with:
files: dist/*
tag_name: v${{ github.event.inputs.version }}

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

@@ -0,0 +1,93 @@
name: test
on:
push:
branches:
- master
- "test-me-*"
pull_request:
branches:
- "*"
# Cancel running jobs for the same workflow and branch.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
package:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build and Check Package
uses: hynek/build-and-inspect-python-package@v2.13
test:
needs: [package]
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
tox_env:
- "py39-pytestmin"
- "py39-pytestlatest"
- "py310-pytestlatest"
- "py311-pytestlatest"
- "py311-pytestmain"
- "py312-pytestlatest"
- "py313-pytestlatest"
- "py310-psutil"
- "py310-setproctitle"
os: [ubuntu-latest, windows-latest]
include:
- tox_env: "py39-pytestmin"
python: "3.9"
- tox_env: "py39-pytestlatest"
python: "3.9"
- tox_env: "py310-pytestlatest"
python: "3.10"
- tox_env: "py311-pytestlatest"
python: "3.11"
- tox_env: "py311-pytestmain"
python: "3.11"
- tox_env: "py312-pytestlatest"
python: "3.12"
- tox_env: "py313-pytestlatest"
python: "3.13"
- tox_env: "py310-psutil"
python: "3.10"
- tox_env: "py310-setproctitle"
python: "3.10"
steps:
- uses: actions/checkout@v4
with:
# Needed to fetch tags, which are required by setuptools-scm.
fetch-depth: 0
- name: Download Package
uses: actions/download-artifact@v4
with:
name: Packages
path: dist
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python }}
- name: Install tox
run: |
python -m pip install --upgrade pip
pip install tox
- name: Test
shell: bash
run: |
tox run -e ${{ matrix.tox_env }} --installpkg `find dist/*.tar.gz`

5
.gitignore vendored
View File

@@ -22,11 +22,14 @@ dist/
include/
lib/
bin/
xdist/_version.py*
env/
src/xdist/_version.py*
pytest_xdist.egg-info
issue/
3rdparty/
pytestdebug.log
.tox/
.cache/
.pytest_cache/
.eggs/
.idea/

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

@@ -0,0 +1,35 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: "v0.12.3"
hooks:
- id: ruff
args: ["--fix"]
- id: ruff-format
- repo: https://github.com/asottile/blacken-docs
rev: 1.19.1
hooks:
- id: blacken-docs
additional_dependencies: [black==23.1.0]
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: check-yaml
- repo: local
hooks:
- id: rst
name: rst
entry: rst-lint --encoding utf-8
files: ^(CHANGELOG.rst|HOWTORELEASE.rst|README.rst|changelog/.*)$
language: python
additional_dependencies: [pygments, restructuredtext_lint]
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.16.1
hooks:
- id: mypy
files: ^(src/|testing/)
args: []
additional_dependencies:
- pytest>=7.0.0
- execnet>=2.1.0
- types-psutil
- setproctitle

14
.readthedocs.yaml Normal file
View File

@@ -0,0 +1,14 @@
version: 2
build:
os: ubuntu-22.04
tools:
python: "3.11"
sphinx:
configuration: docs/conf.py
python:
install:
- path: .
- requirements: docs/requirements.txt

View File

@@ -1,36 +0,0 @@
sudo: false
language: python
python:
- '2.6'
- '2.7'
- '3.4'
- '3.5'
- '3.6'
install: pip install tox-travis setuptools_scm
matrix:
include:
# note: please use "tox --listenvs" to populate the build matrix
- python: "3.5"
env: TOXENV=flakes
- python: "3.5"
env: TOXENV=readme
script: tox
notifications:
irc:
channels:
- chat.freenode.net#pytest
on_success: change
on_failure: change
skip_join: true
email:
- pytest-commit@python.org
deploy:
provider: pypi
user: ronny
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
distributions: sdist bdist_wheel
repo: pytest-dev/pytest-xdist

226
CHANGELOG
View File

@@ -1,226 +0,0 @@
1.17.0
------
- fix #124: xdist would mark test as complete after 'call' step. As a result,
xdist could identify the wrong test as failing when test crashes at teardown.
To address this issue, xdist now marks test as complete at teardown.
1.16.0
------
- ``pytest-xdist`` now requires pytest 2.7 or later.
- Add ``worker_id`` attribute in the TestReport
- new hook: ``pytest_xdist_make_scheduler(config, log)``, can return custom tests items
distribution logic implementation. You can take a look at built-in ``LoadScheduling``
and ``EachScheduling`` implementations. Note that required scheduler class public
API may change in next ``pytest-xdist`` versions.
1.15.0
------
- new ``worker_id`` fixture, returns the id of the worker in a test or fixture.
Thanks Jared Hellman for the PR.
- display progress during collection only when in a terminal, similar to pytest #1397 issue.
Thanks Bruno Oliveira for the PR.
- fix internal error message when ``--maxfail`` is used (#62, #65).
Thanks Collin RM Stocks and Bryan A. Jones for reports and Bruno Oliveira for the PR.
1.14
----
- new hook: ``pytest_xdist_node_collection_finished(node, ids)``, called when
a worker has finished collection. Thanks Omer Katz for the request and
Bruno Oliveira for the PR.
- fix README display on pypi
- fix #22: xdist now works if the internal tmpdir plugin is disabled.
Thanks Bruno Oliveira for the PR.
- fix #32: xdist now works if looponfail or boxed are disabled.
Thanks Bruno Oliveira for the PR.
1.13.1
-------
- fix a regression -n 0 now disables xdist again
1.13
-------------------------
- extended the tox matrix with the supported py.test versions
- split up the plugin into 3 plugin's
to prepare the departure of boxed and looponfail.
looponfail will be a part of core
and forked boxed will be replaced
with a more reliable primitive based on xdist
- conforming with new pytest-2.8 behavior of returning non-zero when all
tests were skipped or deselected.
- new "--max-slave-restart" option that can be used to control maximum
number of times pytest-xdist can restart slaves due to crashes. Thanks to
Anatoly Bubenkov for the report and Bruno Oliveira for the PR.
- release as wheel
- "-n" option now can be set to "auto" for automatic detection of number
of cpus in the host system. Thanks Suloev Dmitry for the PR.
1.12
-------------------------
- fix issue594: properly report errors when the test collection
is random. Thanks Bruno Oliveira.
- some internal test suite adaptation (to become forward
compatible with the upcoming pytest-2.8)
1.11
-------------------------
- fix pytest/xdist issue485 (also depends on py-1.4.22):
attach stdout/stderr on --boxed processes that die.
- fix pytest/xdist issue503: make sure that a node has usually
two items to execute to avoid scoped fixtures to be torn down
pre-maturely (fixture teardown/setup is "nextitem" sensitive).
Thanks to Andreas Pelme for bug analysis and failing test.
- restart crashed nodes by internally refactoring setup handling
of nodes. Also includes better code documentation.
Many thanks to Floris Bruynooghe for the complete PR.
1.10
-------------------------
- add glob support for rsyncignores, add command line option to pass
additional rsyncignores. Thanks Anatoly Bubenkov.
- fix pytest issue382 - produce "pytest_runtest_logstart" event again
in master. Thanks Aron Curzon.
- fix pytest issue419 by sending/receiving indices into the test
collection instead of node ids (which are not neccessarily unique
for functions parametrized with duplicate values)
- send multiple "to test" indices in one network message to a slave
and improve heuristics for sending chunks where the chunksize
depends on the number of remaining tests rather than fixed numbers.
This reduces the number of master -> node messages (but not the
reverse direction)
1.9
-------------------------
- changed LICENSE to MIT
- fix duplicate reported test ids with --looponfailing
(thanks Jeremy Thurgood)
- fix pytest issue41: re-run tests on all file changes, not just
randomly select ones like .py/.c.
- fix pytest issue347: slaves running on top of Python3.2
will set PYTHONDONTWRITEYBTECODE to 1 to avoid import concurrency
bugs.
1.8
-------------------------
- fix pytest-issue93 - use the refined pytest-2.2.1 runtestprotocol
interface to perform eager teardowns for test items.
1.7
-------------------------
- fix incompatibilities with pytest-2.2.0 (allow multiple
pytest_runtest_logreport reports for a test item)
1.6
-------------------------
- terser collection reporting
- fix issue34 - distributed testing with -p plugin now works correctly
- fix race condition in looponfail mode where a concurrent file removal
could cause a crash
1.5
-------------------------
- adapt to and require pytest-2.0 changes, rsyncdirs and rsyncignore can now
only be specified in [pytest] sections of ini files, see "py.test -h"
for details.
- major internal refactoring to match the pytest-2.0 event refactoring
- perform test collection always at slave side instead of at the master
- make python2/python3 bridging work, remove usage of pickling
- improve initial reporting by using line-rewriting
- remove all trailing whitespace from source
1.4
-------------------------
- perform distributed testing related reporting in the plugin
rather than having dist-related code in the generic py.test
distribution
- depend on execnet-1.0.7 which adds "env1:NAME=value" keys to
gateway specification strings.
- show detailed gateway setup and platform information only when
"-v" or "--verbose" is specified.
1.3
-------------------------
- fix --looponfailing - it would not actually run against the fully changed
source tree when initial conftest files load application state.
- adapt for py-1.3.1's new --maxfailure option
1.2
-------------------------
- fix issue79: sessionfinish/teardown hooks are now called systematically
on the slave side
- introduce a new data input/output mechanism to allow the master side
to send and receive data from a slave.
- fix race condition in underlying pickling/unpickling handling
- use and require new register hooks facility of py.test>=1.3.0
- require improved execnet>=1.0.6 because of various race conditions
that can arise in xdist testing modes.
- fix some python3 related pickling related race conditions
- fix PyPI description
1.1
-------------------------
- fix an indefinite hang which would wait for events although no events
are pending - this happened if items arrive very quickly while
the "reschedule-event" tried unconditionally avoiding a busy-loop
and not schedule new work.
1.0
-------------------------
- moved code out of py-1.1.1 into its own plugin
- use a new, faster and more sensible model to do load-balancing
of tests - now no magic "MAXITEMSPERHOST" is needed and load-testing
works effectively even with very few tests.
- cleaned up termination handling
- make -x cause hard killing of test nodes to decrease wait time
until the traceback shows up on first failure

1141
CHANGELOG.rst Normal file

File diff suppressed because it is too large Load Diff

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 py.test 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.

36
LICENSE
View File

@@ -1,19 +1,21 @@
MIT License
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Copyright (c) 2010 Holger Krekel and contributors.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,7 +0,0 @@
include CHANGELOG
include LICENSE
include README.txt
include setup.py
include tox.ini
graft testing
prune .git

View File

@@ -1,78 +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,270 +1,33 @@
============
pytest-xdist
============
.. image:: http://img.shields.io/pypi/v/pytest-xdist.svg
:alt: PyPI version
:target: https://pypi.python.org/pypi/pytest-xdist
.. image:: https://anaconda.org/conda-forge/pytest-xdist/badges/version.svg
.. image:: https://img.shields.io/conda/vn/conda-forge/pytest-xdist.svg
:target: https://anaconda.org/conda-forge/pytest-xdist
.. image:: https://travis-ci.org/pytest-dev/pytest-xdist.svg?branch=master
:target: https://travis-ci.org/pytest-dev/pytest-xdist
.. image:: https://img.shields.io/pypi/pyversions/pytest-xdist.svg
:alt: Python versions
:target: https://pypi.python.org/pypi/pytest-xdist
.. image:: https://ci.appveyor.com/api/projects/status/56eq1a1avd4sdd7e/branch/master?svg=true
:target: https://ci.appveyor.com/project/pytestbot/pytest-xdist
.. image:: https://github.com/pytest-dev/pytest-xdist/workflows/test/badge.svg
:target: https://github.com/pytest-dev/pytest-xdist/actions
xdist: pytest distributed testing plugin
=========================================
.. image:: https://img.shields.io/badge/code%20style-black-000000.svg
:target: https://github.com/ambv/black
The `pytest-xdist`_ plugin extends py.test with some unique
test execution modes:
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::
* test run parallelization_: if you have multiple CPUs or hosts you can use
those for a combined test run. This allows to speed up
development or to use special resources of `remote machines`_.
pytest -n auto
* ``--boxed``: (not available on Windows) run each test in a boxed_
subprocess to survive ``SEGFAULTS`` or otherwise dying processes
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.
* ``--looponfail``: run your tests repeatedly in a subprocess. After each run
py.test waits until a file in your project changes and then re-runs
the previously failing tests. This is repeated until all tests pass
after which again a full run is performed.
Documentation
=============
* `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, ``py.test`` 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.
Installation
-----------------------
Install the plugin with::
easy_install pytest-xdist
# or
pip install pytest-xdist
or use the package in develop/in-place mode with
a checkout of the `pytest-xdist repository`_ ::
python setup.py develop
Usage examples
---------------------
.. _parallelization:
Speed up test runs by sending tests to multiple CPUs
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
To send tests to multiple CPUs, type::
py.test -n NUM
Especially for longer running tests or tests requiring
a lot of IO 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 slave and report the failure as usual. You can use the
``--max-slave-restart`` option to limit the number of slaves that can
be restarted, or disable restarting altogether using ``--max-slave-restart=0``.
Running tests in a Python subprocess
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
To instantiate a python2.5 sub process and send tests to it, you may type::
py.test -d --tx popen//python=python2.5
This will start a subprocess which is run with the "python2.5"
Python interpreter, found in your system binary lookup path.
If you prefix the --tx option value like this::
--tx 3*popen//python=python2.5
then three subprocesses would be created and tests
will be load-balanced across these three processes.
.. _boxed:
Running tests in a boxed subprocess
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
If you have tests involving C or C++ libraries you might have to deal
with tests crashing the process. For this case you may use the boxing
options::
py.test --boxed
which will run each test in a subprocess and will report if a test
crashed the process. You can also combine this option with
running multiple processes to speed up the test run and use your CPU cores::
py.test -n3 --boxed
this would run 3 testing subprocesses in parallel which each
create new boxed subprocesses for each test.
.. _`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::
py.test -d --tx ssh=myhostpopen --rsyncdir mypkg mypkg
This will synchronize your :code:`mypkg` package directory
to an 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 py.test 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
py.test 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::
py.test -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::
py.test --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
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
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 worker_id attribute.
*New in version 1.15.*
Specifying test exec environments in an ini file
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
pytest (since version 2.0) supports ini-style cofiguration.
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=python2.5 --tx ssh=myhost//python=python2.6
and then just type::
py.test --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
Issue and Bug Tracker
------------------------
Please use the `pytest issue tracker <https://github.com/pytest-dev/pytest/issues>`_
for reporting bugs in this plugin.
Documentation is available at `Read The Docs <https://pytest-xdist.readthedocs.io>`__.

41
RELEASING.rst Normal file
View File

@@ -0,0 +1,41 @@
======================
Releasing pytest-xdist
======================
This document describes the steps to make a new ``pytest-xdist`` release.
Version
-------
``master`` should always be green and a potential release candidate. ``pytest-xdist`` follows
semantic versioning, so given that the current version is ``X.Y.Z``, to find the next version number
one needs to look at the ``changelog`` folder:
- If there is any file named ``*.feature``, then we must make a new **minor** release: next
release will be ``X.Y+1.0``.
- Otherwise it is just a **bug fix** release: ``X.Y.Z+1``.
Steps
-----
To publish a new release ``X.Y.Z``, the steps are as follows:
#. Create a new branch named ``release-X.Y.Z`` from the latest ``master``.
#. Install ``tox`` in a virtualenv::
$ pip install tox
#. Update the necessary files with::
$ tox -e release -- X.Y.Z
#. Commit and push the branch to ``upstream`` and open a PR.
#. Once the PR is **green** and **approved**, start the ``deploy`` workflow manually from the branch ``release-VERSION``, passing ``VERSION`` as parameter, or execute::
gh workflow run deploy.yml -R pytest-dev/pytest-xdist --ref release-X.Y.Z --field version=X.Y.Z
#. Merge the release PR to ``master``.

View File

@@ -1,35 +0,0 @@
environment:
matrix:
# note: please use "tox --listenvs" to populate the build matrix
- TOXENV: "py26-pytest27"
- TOXENV: "py26-pytest28"
- TOXENV: "py26-pytest29"
- TOXENV: "py26-pytest30"
- TOXENV: "py27-pytest27"
- TOXENV: "py27-pytest28"
- TOXENV: "py27-pytest29"
- TOXENV: "py27-pytest30"
- TOXENV: "py34-pytest27"
- TOXENV: "py34-pytest28"
- TOXENV: "py34-pytest29"
- TOXENV: "py34-pytest30"
- TOXENV: "py35-pytest27"
- TOXENV: "py35-pytest28"
- TOXENV: "py35-pytest29"
- TOXENV: "py35-pytest30"
- TOXENV: "py36-pytest27"
- TOXENV: "py36-pytest28"
- TOXENV: "py36-pytest29"
- TOXENV: "py36-pytest30"
- TOXENV: "py27-pytest28-pexpect"
- TOXENV: "py35-pytest28-pexpect"
- TOXENV: "flakes"
- TOXENV: "readme"
install:
- C:\Python35\python -m pip install tox setuptools_scm
build: false # Not a C# project, build stuff at the test step instead.
test_script:
- C:\Python35\python -m tox

38
changelog/_template.rst Normal file
View File

@@ -0,0 +1,38 @@
{% for section in sections %}
{% set underline = "-" %}
{% if section %}
{{section}}
{{ underline * section|length }}{% set underline = "~" %}
{% endif %}
{% if sections[section] %}
{% for category, val in definitions.items() if category in sections[section] %}
{{ definitions[category]['name'] }}
{{ underline * definitions[category]['name']|length }}
{% if definitions[category]['showcontent'] %}
{% for text, values in sections[section][category]|dictsort(by='value') %}
- `{{ values[0] }} <https://github.com/pytest-dev/pytest-xdist/issues/{{ values[0][1:] }}>`_: {{ text }}
{% endfor %}
{% else %}
- {{ sections[section][category]['']|sort|join(', ') }}
{% endif %}
{% if sections[section][category]|length == 0 %}
No significant changes.
{% else %}
{% endif %}
{% endfor %}
{% else %}
No significant changes.
{% endif %}
{% endfor %}

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 = "2010, 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``.

130
docs/distribution.rst Normal file
View File

@@ -0,0 +1,130 @@
.. _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 physical CPU cores.
Use ``-n logical`` to use the number of *logical* CPU cores rather than
physical ones. This currently requires the `psutil <https://pypi.org/project/psutil/>`__ package to be installed;
if it is not or if it fails to determine the number of logical CPUs, fall back to ``-n auto`` behavior.
Pass a number, e.g. ``-n 8``, to specify the number of processes explicitly.
Use ``-n 0`` to disable xdist and run all tests in the main process.
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. Scheduling can be fine-tuned with
the `--maxschedchunk` option, see output of `pytest --help`.
* ``--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. If a test has
multiple groups, they will be joined together into a new group,
the order of the marks doesn't matter. This works along with marks from fixtures
and from the pytestmark global variable.
.. 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.
.. code-block:: python
@pytest.fixture(
scope="session",
params=[
pytest.param(
"chrome",
marks=pytest.mark.xdist_group("chrome"),
),
pytest.param(
"firefox",
marks=pytest.mark.xdist_group("firefox"),
),
pytest.param(
"edge",
marks=pytest.mark.xdist_group("edge"),
),
],
)
def setup_container():
pass
@pytest.mark.xdist_group(name="data-store")
def test_data_store(setup_container):
...
This will generate 3 new groups: ``chrome_data-store``, ``data-store_firefox`` and ``data-store_edge`` (the markers are lexically sorted before being merged together).
Tests without the ``xdist_group`` mark are distributed normally as in the ``--dist=load`` mode.
* ``--dist worksteal``: Initially, tests are distributed evenly among all
available workers. When a worker completes most of its assigned tests and
doesn't have enough tests to continue (currently, every worker needs at least
two tests in its queue), an attempt is made to reassign ("steal") a portion
of tests from some other worker's queue. The results should be similar to
the ``load`` method, but ``worksteal`` should handle tests with significantly
differing duration better, and, at the same time, it should provide similar
or better reuse of fixtures.
* ``--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* instead 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.

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

@@ -0,0 +1,232 @@
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:
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,71 @@
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
-------------------------------------------
It 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.
Currently, there are no plans to support this in ``pytest-xdist``.
Debugging
~~~~~~~~~
This also means that debugging using PDB (or any other debugger that wants to use standard I/O) will not work. The ``--pdb`` option is disabled when distributing tests with ``pytest-xdist`` for this reason.
It is generally likely best to use ``pytest-xdist`` to find failing tests and then debug them without distribution; however, if you need to debug from within a worker process (for example, to address failures that only happen when running tests concurrently), remote debuggers (for example, `python-remote-pdb <https://github.com/ionelmc/python-remote-pdb>`__ or `python-web-pdb <https://github.com/romanvm/python-web-pdb>`__) have been reported to work for this purpose.

100
docs/remote.rst Normal file
View File

@@ -0,0 +1,100 @@
.. _`Multi-Platform`:
.. _`remote machines`:
Sending tests to remote SSH accounts
====================================
.. deprecated:: 3.0
.. warning::
The ``rsync`` feature is deprecated because its implementation is faulty
in terms of reproducing the development environment in the remote
worker, and there is no clear solution moving forward.
For that reason, ``rsync`` is scheduled to be removed in release 4.0, to let the team
focus on a smaller set of features.
Note that SSH and socket server are not planned for removal, as they are part
of the ``execnet`` feature set.
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
Using proxies to run multiple workers on remote machines
---------------------------------------
In case you want to run multiple workers on a remote machine,
you can create a proxy gateway for the machine, and run multiple
workers using the `via` attribute.::
pytest -d --px id=my_proxy//socket=192.168.1.102:8888 --tx 5*popen//via=my_proxy
Here we declare a proxy gateway using the `--px` arg, and
create 5 workers that run on the remote server using the proxy.
Note that the proxy gateway does not run a worker, thus only 5
workers are created.
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/src/execnet/script/socketserver.py

2
docs/requirements.txt Normal file
View File

@@ -0,0 +1,2 @@
sphinx
sphinx-rtd-theme

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,3 +1,9 @@
.. warning::
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>`_
which can be installed independently. The ``--boxed`` command-line option is deprecated
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
@@ -14,19 +20,19 @@ to run each test in a controlled subprocess. Here is a basic example::
@pytest.mark.parametrize("arg", range(50))
def test_func(arg):
time.sleep(0.05) # each tests takes a while
if arg % 19 == 0:
if arg % 19 == 0:
os.kill(os.getpid(), 15)
If you run this with::
$ py.test --boxed
$ pytest -n1
=========================== test session starts ============================
platform linux2 -- Python 2.7.3 -- pytest-2.3.0.dev8
plugins: xdist, bugzilla, cache, oejskit, cli, pep8, cov
collecting ... collected 50 items
test_module.py f..................f..................f...........
================================= FAILURES =================================
_______________________________ test_func[0] _______________________________
/home/hpk/tmp/doc-exec-420/test_module.py:6: running the test CRASHED with signal 15
@@ -40,13 +46,13 @@ You'll see that a couple of tests are reported as crashing, indicated
by lower-case ``f`` and the respective failure summary. You can also use
the xdist-provided parallelization feature to speed up your testing::
$ py.test --boxed -n3
$ pytest -n3
=========================== test session starts ============================
platform linux2 -- Python 2.7.3 -- pytest-2.3.0.dev8
plugins: xdist, bugzilla, cache, oejskit, cli, pep8, cov
gw0 I / gw1 I / gw2 I
gw0 [50] / gw1 [50] / gw2 [50]
scheduling tests via LoadScheduling
..f...............f..................f............
================================= FAILURES =================================

View File

@@ -0,0 +1,37 @@
def epsilon1(arg1, arg2=1000):
"""Do epsilon1
Usage:
>>> epsilon1(10, 20)
40
>>> epsilon1(30)
1040
"""
return arg1 + arg2 + 10
def epsilon2(arg1, arg2=1000):
"""Do epsilon2
Usage:
>>> epsilon2(10, 20)
-20
>>> epsilon2(30)
-980
"""
return arg1 - arg2 - 10
def epsilon3(arg1, arg2=1000):
"""Do epsilon3
Usage:
>>> epsilon3(10, 20)
200
>>> epsilon3(30)
30000
"""
return arg1 * arg2

View File

@@ -0,0 +1,3 @@
ipdb
pytest
../../

View File

@@ -0,0 +1,51 @@
from time import sleep
def test_alpha0():
sleep(5)
assert True
def test_alpha1():
sleep(5)
assert True
def test_alpha2():
sleep(5)
assert True
def test_alpha3():
sleep(5)
assert True
def test_alpha4():
sleep(5)
assert True
def test_alpha5():
sleep(5)
assert True
def test_alpha6():
sleep(5)
assert True
def test_alpha7():
sleep(5)
assert True
def test_alpha8():
sleep(5)
assert True
def test_alpha9():
sleep(5)
assert True

View File

@@ -0,0 +1,51 @@
from time import sleep
def test_beta0():
sleep(5)
assert True
def test_beta1():
sleep(5)
assert True
def test_beta2():
sleep(5)
assert True
def test_beta3():
sleep(5)
assert True
def test_beta4():
sleep(5)
assert True
def test_beta5():
sleep(5)
assert True
def test_beta6():
sleep(5)
assert True
def test_beta7():
sleep(5)
assert True
def test_beta8():
sleep(5)
assert True
def test_beta9():
sleep(5)
assert True

View File

@@ -0,0 +1,86 @@
from time import sleep
from unittest import TestCase
class Delta1(TestCase):
def test_delta0(self):
sleep(5)
assert True
def test_delta1(self):
sleep(5)
assert True
def test_delta2(self):
sleep(5)
assert True
def test_delta3(self):
sleep(5)
assert True
def test_delta4(self):
sleep(5)
assert True
def test_delta5(self):
sleep(5)
assert True
def test_delta6(self):
sleep(5)
assert True
def test_delta7(self):
sleep(5)
assert True
def test_delta8(self):
sleep(5)
assert True
def test_delta9(self):
sleep(5)
assert True
class Delta2(TestCase):
def test_delta0(self):
sleep(5)
assert True
def test_delta1(self):
sleep(5)
assert True
def test_delta2(self):
sleep(5)
assert True
def test_delta3(self):
sleep(5)
assert True
def test_delta4(self):
sleep(5)
assert True
def test_delta5(self):
sleep(5)
assert True
def test_delta6(self):
sleep(5)
assert True
def test_delta7(self):
sleep(5)
assert True
def test_delta8(self):
sleep(5)
assert True
def test_delta9(self):
sleep(5)
assert True

View File

@@ -0,0 +1,51 @@
from time import sleep
def test_gamma0():
sleep(5)
assert True
def test_gamma1():
sleep(5)
assert True
def test_gamma2():
sleep(5)
assert True
def test_gamma3():
sleep(5)
assert True
def test_gamma4():
sleep(5)
assert True
def test_gamma5():
sleep(5)
assert True
def test_gamma6():
sleep(5)
assert True
def test_gamma7():
sleep(5)
assert True
def test_gamma8():
sleep(5)
assert True
def test_gamma9():
sleep(5)
assert True

17
example/loadscope/tox.ini Normal file
View File

@@ -0,0 +1,17 @@
[tox]
envlist = test
setupdir = {toxinidir}/../../
[testenv:test]
basepython = python3
passenv = http_proxy https_proxy
deps = -rrequirements.txt
changedir = {envtmpdir}
commands =
pytest -s -v \
--doctest-modules \
--junitxml=tests.xml \
--dist=loadscope \
--tx=8*popen \
{toxinidir}/test \
{toxinidir}/epsilon

170
pyproject.toml Normal file
View File

@@ -0,0 +1,170 @@
[build-system]
requires = [
"setuptools>=77.0",
"setuptools-scm[toml]>=6.2.3",
]
build-backend = "setuptools.build_meta"
[project]
name = "pytest-xdist"
description = "pytest xdist plugin for distributed testing, most importantly across multiple CPUs"
readme = "README.rst"
license = "MIT"
authors = [{name = "holger krekel and contributors", email = "pytest-dev@python.org"}, {email = "holger@merlinux.eu"}]
classifiers = [
"Development Status :: 5 - Production/Stable",
"Framework :: Pytest",
"Intended Audience :: Developers",
"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.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
]
requires-python = ">=3.9"
dependencies = [
"execnet>=2.1",
"pytest>=7.0.0",
]
dynamic = ["version"]
[project.urls]
Homepage = "https://github.com/pytest-dev/pytest-xdist"
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"
[project.entry-points.pytest11]
xdist = "xdist.plugin"
"xdist.looponfail" = "xdist.looponfail"
[project.optional-dependencies]
testing = ["filelock"]
psutil = ["psutil>=3.0"]
setproctitle = ["setproctitle"]
[tool.setuptools_scm]
write_to = "src/xdist/_version.py"
[tool.pytest.ini_options]
# 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"]
[tool.ruff]
src = ["src"]
[tool.ruff.format]
docstring-code-format = true
[tool.ruff.lint]
select = [
"B", # bugbear
"D", # pydocstyle
"E", # pycodestyle
"F", # pyflakes
"I", # isort
"PYI", # flake8-pyi
"UP", # pyupgrade
"RUF", # ruff
"W", # pycodestyle
"T10", # flake8-debugger
"PIE", # flake8-pie
"FA", # flake8-future-annotations
"PGH", # pygrep-hooks
"PLE", # pylint error
"PLW", # pylint warning
"PLR1714", # Consider merging multiple comparisons
]
ignore = [
# bugbear ignore
"B011", # Do not `assert False` (`python -O` removes these calls)
"B028", # No explicit `stacklevel` keyword argument found
# pydocstyle ignore
"D100", # Missing docstring in public module
"D101", # Missing docstring in public class
"D102", # Missing docstring in public method
"D103", # Missing docstring in public function
"D104", # Missing docstring in public package
"D105", # Missing docstring in magic method
"D106", # Missing docstring in public nested class
"D107", # Missing docstring in `__init__`
"D209", # Multi-line docstring closing quotes should be on a separate line
"D205", # 1 blank line required between summary line and description
"D400", # First line should end with a period
"D401", # First line of docstring should be in imperative mood
# ruff ignore
"RUF012", # Mutable class attributes should be annotated with `typing.ClassVar`
# pylint ignore
"PLW0603", # Using the global statement
"PLW0120", # remove the else and dedent its contents
"PLW2901", # for loop variable overwritten by assignment target
"PLR5501", # Use `elif` instead of `else` then `if`
"UP031", # Use format specifiers instead of percent format
]
[tool.ruff.lint.pycodestyle]
# In order to be able to format for 88 char in ruff format
max-line-length = 120
[tool.ruff.lint.pydocstyle]
convention = "pep257"
[tool.ruff.lint.isort]
force-single-line = true
combine-as-imports = true
force-sort-within-sections = true
order-by-type = false
lines-after-imports = 2
[tool.ruff.lint.per-file-ignores]
"src/xdist/_version.py" = ["I001"]
[tool.mypy]
mypy_path = ["src"]
files = ["src", "testing"]
strict = true
warn_unreachable = true
[[tool.mypy.overrides]]
module = ["xdist._version"]
ignore_missing_imports = true
[tool.towncrier]
package = "xdist"
filename = "CHANGELOG.rst"
directory = "changelog/"
title_format = "pytest-xdist {version} ({project_date})"
template = "changelog/_template.rst"
[tool.towncrier.fragment.removal]
name = "Removals"
[tool.towncrier.fragment.deprecation]
name = "Deprecations"
[tool.towncrier.fragment.feature]
name = "Features"
[tool.towncrier.fragment.bugfix]
name = "Bug Fixes"
[tool.towncrier.fragment.vendor]
name = "Vendored Libraries"
[tool.towncrier.fragment.doc]
name = "Improved Documentation"
[tool.towncrier.fragment.trivial]
name = "Trivial Changes"

View File

@@ -1,5 +0,0 @@
[bdist_wheel]
universal = 1
[metadata]
license_file = LICENSE

View File

@@ -1,39 +0,0 @@
from setuptools import setup
setup(
name="pytest-xdist",
use_scm_version={'write_to': 'xdist/_version.py'},
description='py.test 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=['xdist'],
entry_points={
'pytest11': [
'xdist = xdist.plugin',
'xdist.looponfail = xdist.looponfail',
'xdist.boxed = xdist.boxed',
],
},
zip_safe=False,
install_requires=['execnet>=1.1', 'pytest>=2.7.0', 'py>=1.4.22'],
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 :: 3',
],
)

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

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

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

@@ -0,0 +1,20 @@
from collections.abc import Iterator
from itertools import chain
import os
from pathlib import Path
from typing import Callable
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

601
src/xdist/dsession.py Normal file
View File

@@ -0,0 +1,601 @@
from __future__ import annotations
from collections.abc import Sequence
from enum import auto
from enum import Enum
from queue import Empty
from queue import Queue
import sys
from typing import Any
import warnings
import execnet
import pytest
from xdist.remote import Producer
from xdist.remote import WorkerInfo
from xdist.scheduler import EachScheduling
from xdist.scheduler import LoadFileScheduling
from xdist.scheduler import LoadGroupScheduling
from xdist.scheduler import LoadScheduling
from xdist.scheduler import LoadScopeScheduling
from xdist.scheduler import Scheduling
from xdist.scheduler import WorkStealingScheduling
from xdist.workermanage import NodeManager
from xdist.workermanage import WorkerController
class Interrupted(KeyboardInterrupt):
"""signals an immediate interruption."""
class DSession:
"""A pytest plugin which runs a distributed test session.
At the beginning of the test session this creates a NodeManager
instance which creates and starts all nodes. Nodes then emit
events processed in the pytest_runtestloop hook using the worker_*
methods.
Once a node is started it will automatically start running the
pytest mainloop with some custom hooks. This means a node
automatically starts collecting tests. Once tests are collected
it will wait for instructions.
"""
shouldstop: bool | str
def __init__(self, config: pytest.Config) -> None:
self.config = config
self.log = Producer("dsession", enabled=config.option.debug)
self.nodemanager: NodeManager | None = None
self.sched: Scheduling | None = None
self.shuttingdown = False
self.countfailures = 0
self.maxfail: int = config.getvalue("maxfail")
self.queue: Queue[tuple[str, dict[str, Any]]] = Queue()
self._session: pytest.Session | None = None
self._failed_collection_errors: dict[object, bool] = {}
self._active_nodes: set[WorkerController] = set()
self._failed_nodes_count = 0
self._max_worker_restart = get_default_max_worker_restart(self.config)
# summary message to print at the end of the session
self._summary_report: str | None = None
self.terminal = config.pluginmanager.getplugin("terminalreporter")
if self.terminal:
self.trdist = TerminalDistReporter(config)
config.pluginmanager.register(self.trdist, "terminaldistreporter")
@property
def session_finished(self) -> bool:
"""Return True if the distributed session has finished.
This means all nodes have executed all test items. This is
used by pytest_runtestloop to break out of its loop.
"""
return bool(self.shuttingdown and not self._active_nodes)
def report_line(self, line: str) -> None:
if self.terminal and self.config.option.verbose >= 0:
self.terminal.write_line(line)
@pytest.hookimpl(trylast=True)
def pytest_sessionstart(self, session: pytest.Session) -> None:
"""Creates and starts the nodes.
The nodes are setup to put their events onto self.queue. As
soon as nodes start they will emit the worker_workerready event.
"""
self.nodemanager = NodeManager(self.config)
nodes = self.nodemanager.setup_nodes(putevent=self.queue.put)
self._active_nodes.update(nodes)
self._session = session
@pytest.hookimpl
def pytest_sessionfinish(self) -> None:
"""Shutdown all nodes."""
nm = getattr(self, "nodemanager", None) # if not fully initialized
if nm is not None:
nm.teardown_nodes()
self._session = None
@pytest.hookimpl
def pytest_collection(self) -> bool:
# prohibit collection of test items in controller process
return True
@pytest.hookimpl(trylast=True)
def pytest_xdist_make_scheduler(
self,
config: pytest.Config,
log: Producer,
) -> Scheduling | None:
dist = config.getvalue("dist")
if dist == "each":
return EachScheduling(config, log)
if dist == "load":
return LoadScheduling(config, log)
if dist == "loadscope":
return LoadScopeScheduling(config, log)
if dist == "loadfile":
return LoadFileScheduling(config, log)
if dist == "loadgroup":
return LoadGroupScheduling(config, log)
if dist == "worksteal":
return WorkStealingScheduling(config, log)
return None
@pytest.hookimpl
def pytest_runtestloop(self) -> bool:
self.sched = self.config.hook.pytest_xdist_make_scheduler(
config=self.config, log=self.log
)
assert self.sched is not None
self.shouldstop = False
pending_exception = None
while not self.session_finished:
self.loop_once()
if self.shouldstop:
self.triggershutdown()
pending_exception = Interrupted(str(self.shouldstop))
if pending_exception:
raise pending_exception
return True
def loop_once(self) -> None:
"""Process one callback from one of the workers."""
while 1:
if not self._active_nodes:
# If everything has died stop looping
self.triggershutdown()
raise RuntimeError("Unexpectedly no active workers available")
try:
eventcall = self.queue.get(timeout=2.0)
break
except Empty:
continue
callname, kwargs = eventcall
assert callname, kwargs
method = "worker_" + callname
call = getattr(self, method)
self.log("calling method", method, kwargs)
call(**kwargs)
assert self.sched is not None
if self.sched.tests_finished:
self.triggershutdown()
#
# callbacks for processing events from workers
#
def worker_workerready(
self,
node: WorkerController,
workerinfo: WorkerInfo,
) -> None:
"""Emitted when a node first starts up.
This adds the node to the scheduler, nodes continue with
collection without any further input.
"""
node.workerinfo = workerinfo
node.workerinfo["id"] = node.gateway.id
node.workerinfo["spec"] = node.gateway.spec
self.config.hook.pytest_testnodeready(node=node)
if self.shuttingdown:
node.shutdown()
else:
assert self.sched is not None
self.sched.add_node(node)
def worker_workerfinished(self, node: WorkerController) -> None:
"""Emitted when node executes its pytest_sessionfinish hook.
Removes the node from the scheduler.
The node might not be in the scheduler if it had not emitted
workerready before shutdown was triggered.
"""
self.config.hook.pytest_testnodedown(node=node, error=None)
if node.workeroutput["exitstatus"] == 2: # keyboard-interrupt
self.shouldstop = f"{node} received keyboard-interrupt"
self.worker_errordown(node, "keyboard-interrupt")
return
shouldfail = node.workeroutput["shouldfail"]
shouldstop = node.workeroutput["shouldstop"]
for shouldx in [shouldfail, shouldstop]:
if shouldx:
if not self.shouldstop:
self.shouldstop = shouldx
break
else:
assert self.sched is not None
if node in self.sched.nodes:
crashitem = self.sched.remove_node(node)
assert not crashitem, (crashitem, node)
self._active_nodes.remove(node)
def worker_internal_error(
self, node: WorkerController, formatted_error: str
) -> None:
"""
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:
excinfo = pytest.ExceptionInfo.from_current()
excrepr = excinfo.getrepr()
self.config.hook.pytest_internalerror(excrepr=excrepr, excinfo=excinfo)
def worker_errordown(self, node: WorkerController, error: object | None) -> None:
"""Emitted by the WorkerController when a node dies."""
self.config.hook.pytest_testnodedown(node=node, error=error)
assert self.sched is not None
try:
crashitem = self.sched.remove_node(node)
except KeyError:
pass
else:
if crashitem:
self.handle_crashitem(crashitem, node)
self._failed_nodes_count += 1
maximum_reached = (
self._max_worker_restart is not None
and self._failed_nodes_count > self._max_worker_restart
)
if maximum_reached:
if self._max_worker_restart == 0:
msg = f"worker {node.gateway.id} crashed and worker restarting disabled"
else:
msg = f"maximum crashed workers reached: {self._max_worker_restart}"
self._summary_report = msg
self.report_line("\n" + msg)
self.triggershutdown()
else:
self.report_line("\nreplacing crashed worker %s" % node.gateway.id)
self.shuttingdown = False
self._clone_node(node)
self._active_nodes.remove(node)
@pytest.hookimpl
def pytest_terminal_summary(self, terminalreporter: Any) -> None:
if self.config.option.verbose >= 0 and self._summary_report:
terminalreporter.write_sep("=", f"xdist: {self._summary_report}")
def worker_collectionfinish(
self, node: WorkerController, ids: Sequence[str]
) -> None:
"""Worker has finished test collection.
This adds the collection for this node to the scheduler. If
the scheduler indicates collection is finished (i.e. all
initial nodes have submitted their collections), then tells the
scheduler to schedule the collected items. When initiating
scheduling the first time it logs which scheduler is in use.
"""
if self.shuttingdown:
return
self.config.hook.pytest_xdist_node_collection_finished(node=node, ids=ids)
# tell session which items were effectively collected otherwise
# the controller node will finish the session with EXIT_NOTESTSCOLLECTED
assert self._session is not None
self._session.testscollected = len(ids)
assert self.sched is not None
self.sched.add_node_collection(node, ids)
if self.terminal:
self.trdist.setstatus(
node.gateway.spec, WorkerStatus.CollectionDone, tests_collected=len(ids)
)
if self.sched.collection_is_completed:
if self.terminal and not self.sched.has_pending:
self.trdist.ensure_show_status()
self.terminal.write_line("")
if self.config.option.verbose > 0:
self.terminal.write_line(
f"scheduling tests via {self.sched.__class__.__name__}"
)
self.sched.schedule()
def worker_logstart(
self,
node: WorkerController,
nodeid: str,
location: tuple[str, int | None, str],
) -> None:
"""Emitted when a node calls the pytest_runtest_logstart hook."""
self.config.hook.pytest_runtest_logstart(nodeid=nodeid, location=location)
def worker_logfinish(
self,
node: WorkerController,
nodeid: str,
location: tuple[str, int | None, str],
) -> None:
"""Emitted when a node calls the pytest_runtest_logfinish hook."""
self.config.hook.pytest_runtest_logfinish(nodeid=nodeid, location=location)
def worker_testreport(self, node: WorkerController, rep: pytest.TestReport) -> None:
"""Emitted when a node calls the pytest_runtest_logreport hook."""
rep.node = node # type: ignore[attr-defined]
self.config.hook.pytest_runtest_logreport(report=rep)
self._handlefailures(rep)
def worker_runtest_protocol_complete(
self, node: WorkerController, item_index: int, duration: float
) -> None:
"""
Emitted when a node fires the 'runtest_protocol_complete' event,
signalling that a test has completed the runtestprotocol and should be
removed from the pending list in the scheduler.
"""
assert self.sched is not None
self.sched.mark_test_complete(node, item_index, duration)
def worker_unscheduled(
self, node: WorkerController, indices: Sequence[int]
) -> None:
"""
Emitted when a node fires the 'unscheduled' event, signalling that
some tests have been removed from the worker's queue and should be
sent to some worker again.
This should happen only in response to 'steal' command, so schedulers
not using 'steal' command don't have to implement it.
"""
assert self.sched is not None
self.sched.remove_pending_tests_from_node(node, indices)
def worker_collectreport(
self,
node: WorkerController,
rep: pytest.CollectReport | pytest.TestReport,
) -> None:
"""Emitted when a node calls the pytest_collectreport hook.
Because we only need the report when there's a failure/skip, as optimization
we only expect to receive failed/skipped reports from workers (#330).
"""
assert not rep.passed
self._failed_worker_collectreport(node, rep)
def worker_warning_recorded(
self,
warning_message: warnings.WarningMessage,
when: str,
nodeid: str,
location: tuple[str, int, str] | None,
) -> None:
"""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: WorkerController) -> WorkerController:
"""Return new node based on an existing one.
This is normally for when a node dies, this will copy the spec
of the existing node and create a new one with a new id. The
new node will have been setup so it will start calling the
"worker_*" hooks and do work soon.
"""
spec = node.gateway.spec
spec.id = None
assert self.nodemanager is not None
self.nodemanager.group.allocate_id(spec)
clone = self.nodemanager.setup_node(spec, self.queue.put)
self._active_nodes.add(clone)
return clone
def _failed_worker_collectreport(
self,
node: WorkerController,
rep: pytest.CollectReport | pytest.TestReport,
) -> None:
# Check we haven't already seen this report (from
# another worker).
if rep.longrepr not in self._failed_collection_errors:
self._failed_collection_errors[rep.longrepr] = True
self.config.hook.pytest_collectreport(report=rep)
self._handlefailures(rep)
def _handlefailures(
self,
rep: pytest.CollectReport | pytest.TestReport,
) -> None:
if rep.failed:
self.countfailures += 1
if (
self.maxfail
and self.countfailures >= self.maxfail
and not self.shouldstop
):
self.shouldstop = f"stopping after {self.countfailures} failures"
def triggershutdown(self) -> None:
if not self.shuttingdown:
self.log("triggering shutdown")
self.shuttingdown = True
assert self.sched is not None
for node in self.sched.nodes:
node.shutdown()
def handle_crashitem(self, nodeid: str, worker: WorkerController) -> None:
# XXX get more reporting info by recording pytest_runtest_logstart?
# XXX count no of failures and retry N times
fspath = nodeid.split("::")[0]
msg = f"worker {worker.gateway.id!r} crashed while running {nodeid!r}"
rep = pytest.TestReport(
nodeid=nodeid,
location=(fspath, None, fspath),
keywords={},
outcome="failed",
longrepr=msg,
when="???", # type: ignore[arg-type]
)
rep.node = worker # type: ignore[attr-defined]
self.config.hook.pytest_handlecrashitem(
crashitem=nodeid,
report=rep,
sched=self.sched,
)
self.config.hook.pytest_runtest_logreport(report=rep)
class WorkerStatus(Enum):
"""Status of each worker during creation/collection."""
# Worker spec has just been created.
Created = auto()
# Worker has been initialized.
Initialized = auto()
# Worker is now ready for collection.
ReadyForCollection = auto()
# Worker has finished collection.
CollectionDone = auto()
class TerminalDistReporter:
def __init__(self, config: pytest.Config) -> None:
self.config = config
self.tr = config.pluginmanager.getplugin("terminalreporter")
self._status: dict[object, tuple[WorkerStatus, int]] = {}
self._lastlen = 0
self._isatty = getattr(self.tr, "isatty", self.tr.hasmarkup)
def write_line(self, msg: str) -> None:
self.tr.write_line(msg)
def ensure_show_status(self) -> None:
if not self._isatty:
self.write_line(self.getstatus())
def setstatus(
self,
spec: execnet.XSpec,
status: WorkerStatus,
*,
tests_collected: int,
show: bool = True,
) -> None:
self._status[spec.id] = (status, tests_collected)
if show and self._isatty:
self.rewrite(self.getstatus())
def getstatus(self) -> str:
if self.config.option.verbose >= 0:
line = get_workers_status_line(list(self._status.values()))
if line:
return line
return "bringing up nodes..."
def rewrite(self, line: str, newline: bool = False) -> None:
pline = line + " " * max(self._lastlen - len(line), 0)
if newline:
self._lastlen = 0
pline += "\n"
else:
self._lastlen = len(line)
self.tr.rewrite(pline, bold=True)
@pytest.hookimpl
def pytest_xdist_setupnodes(self, specs: Sequence[execnet.XSpec]) -> None:
self._specs = specs
for spec in specs:
self.setstatus(spec, WorkerStatus.Created, tests_collected=0, show=False)
self.setstatus(spec, WorkerStatus.Created, tests_collected=0, show=True)
self.ensure_show_status()
@pytest.hookimpl
def pytest_xdist_newgateway(self, gateway: execnet.Gateway) -> None:
if self.config.option.verbose > 0:
rinfo = gateway._rinfo()
different_interpreter = rinfo.executable != sys.executable
if different_interpreter:
version = "{}.{}.{}".format(*rinfo.version_info[:3])
self.rewrite(
f"[{gateway.id}] {rinfo.platform} Python {version} cwd: {rinfo.cwd}",
newline=True,
)
self.setstatus(gateway.spec, WorkerStatus.Initialized, tests_collected=0)
@pytest.hookimpl
def pytest_testnodeready(self, node: WorkerController) -> None:
if self.config.option.verbose > 0:
d = node.workerinfo
different_interpreter = d.get("executable") != sys.executable
if different_interpreter:
version = d["version"].replace("\n", " -- ")
self.rewrite(f"[{d['id']}] Python {version}", newline=True)
self.setstatus(
node.gateway.spec, WorkerStatus.ReadyForCollection, tests_collected=0
)
@pytest.hookimpl
def pytest_testnodedown(self, node: WorkerController, error: object) -> None:
if not error:
return
self.write_line(f"[{node.gateway.id}] node down: {error}")
def get_default_max_worker_restart(config: pytest.Config) -> int | None:
"""Gets the default value of --max-worker-restart option if it is not provided.
Use a reasonable default to avoid workers from restarting endlessly due to crashing collections (#226).
"""
result_str: str | None = config.option.maxworkerrestart
if result_str is not None:
result = int(result_str)
elif config.option.numprocesses:
# if --max-worker-restart was not provided, use a reasonable default (#226)
result = config.option.numprocesses * 4
else:
result = None
return result
def get_workers_status_line(
status_and_items: Sequence[tuple[WorkerStatus, int]],
) -> str:
"""
Return the line to display during worker setup/collection based on the
status of the workers and number of tests collected for each.
"""
statuses = [s for s, c in status_and_items]
total_workers = len(statuses)
workers_noun = "worker" if total_workers == 1 else "workers"
if status_and_items and all(s == WorkerStatus.CollectionDone for s in statuses):
# All workers collect the same number of items, so we grab
# the total number of items from the first worker.
first = status_and_items[0]
status, tests_collected = first
tests_noun = "item" if tests_collected == 1 else "items"
return f"{total_workers} {workers_noun} [{tests_collected} {tests_noun}]"
if WorkerStatus.CollectionDone in statuses:
done = sum(1 for s, c in status_and_items if c > 0)
return f"collecting: {done}/{total_workers} {workers_noun}"
if WorkerStatus.ReadyForCollection in statuses:
ready = statuses.count(WorkerStatus.ReadyForCollection)
return f"ready: {ready}/{total_workers} {workers_noun}"
if WorkerStatus.Initialized in statuses:
initialized = statuses.count(WorkerStatus.Initialized)
return f"initialized: {initialized}/{total_workers} {workers_noun}"
if WorkerStatus.Created in statuses:
created = statuses.count(WorkerStatus.Created)
return f"created: {created}/{total_workers} {workers_noun}"
return ""

299
src/xdist/looponfail.py Normal file
View File

@@ -0,0 +1,299 @@
"""
Implement -f aka looponfailing for pytest.
NOTE that we try to avoid loading and depending on application modules
within the controlling process (the one that starts repeatedly test
processes) otherwise changes to source code can crash
the controlling process which should best never happen.
"""
from __future__ import annotations
from collections.abc import Sequence
import os
from pathlib import Path
import sys
import time
from typing import Any
from _pytest._io import TerminalWriter
import execnet
import pytest
from xdist._path import visit_path
@pytest.hookimpl
def pytest_addoption(parser: pytest.Parser) -> None:
group = parser.getgroup("xdist", "distributed and subprocess testing")
group._addoption(
"-f",
"--looponfail",
action="store_true",
dest="looponfail",
default=False,
help="Run tests in subprocess: wait for files to be modified, then "
"re-run failing test set until all pass.",
)
@pytest.hookimpl
def pytest_cmdline_main(config: pytest.Config) -> int | None:
if config.getoption("looponfail"):
usepdb = config.getoption("usepdb", False) # a core option
if usepdb:
raise pytest.UsageError("--pdb is incompatible with --looponfail.")
looponfail_main(config)
return 2 # looponfail only can get stop with ctrl-C anyway
return None
def looponfail_main(config: pytest.Config) -> None:
remotecontrol = RemoteControl(config)
config_roots = config.getini("looponfailroots")
if not config_roots:
config_roots = [Path.cwd()]
rootdirs = [Path(root) for root in config_roots]
statrecorder = StatRecorder(rootdirs)
try:
while 1:
remotecontrol.loop_once()
if not remotecontrol.failures and remotecontrol.wasfailing:
# the last failures passed, let's immediately rerun all
continue
repr_pytest_looponfailinfo(
failreports=remotecontrol.failures, rootdirs=rootdirs
)
statrecorder.waitonchange(checkinterval=2.0)
except KeyboardInterrupt:
print()
class RemoteControl:
gateway: execnet.Gateway
def __init__(self, config: pytest.Config) -> None:
self.config = config
self.failures: list[str] = []
def trace(self, *args: object) -> None:
if self.config.option.debug:
msg = " ".join(str(x) for x in args)
print("RemoteControl:", msg)
def initgateway(self) -> execnet.Gateway:
return execnet.makegateway("execmodel=main_thread_only//popen")
def setup(self) -> None:
if hasattr(self, "gateway"):
raise ValueError("already have gateway %r" % self.gateway)
self.trace("setting up worker session")
self.gateway = self.initgateway()
self.channel = channel = self.gateway.remote_exec(
init_worker_session,
args=self.config.args,
option_dict=vars(self.config.option),
)
remote_outchannel: execnet.Channel = channel.receive()
out = TerminalWriter()
def write(s: str) -> None:
out._file.write(s)
out._file.flush()
remote_outchannel.setcallback(write)
def ensure_teardown(self) -> None:
if hasattr(self, "channel"):
if not self.channel.isclosed():
self.trace("closing", self.channel)
self.channel.close()
del self.channel
if hasattr(self, "gateway"):
self.trace("exiting", self.gateway)
self.gateway.exit()
del self.gateway
def runsession(self) -> tuple[list[str], list[str], bool]:
try:
self.trace("sending", self.failures)
self.channel.send(self.failures)
try:
return self.channel.receive() # type: ignore[no-any-return]
except self.channel.RemoteError:
e = sys.exc_info()[1]
self.trace("ERROR", e)
raise
finally:
self.ensure_teardown()
def loop_once(self) -> None:
self.setup()
self.wasfailing = self.failures and len(self.failures)
result = self.runsession()
failures, reports, collection_failed = result
if collection_failed:
pass # "Collection failed, keeping previous failure set"
else:
uniq_failures = []
for failure in failures:
if failure not in uniq_failures:
uniq_failures.append(failure)
self.failures = uniq_failures
def repr_pytest_looponfailinfo(
failreports: Sequence[str], rootdirs: Sequence[Path]
) -> None:
tr = TerminalWriter()
if failreports:
tr.sep("#", "LOOPONFAILING", bold=True)
for report in failreports:
if report:
tr.line(report, red=True)
tr.sep("#", "waiting for changes", bold=True)
for rootdir in rootdirs:
tr.line(f"### Watching: {rootdir}", bold=True)
def init_worker_session(
channel: "execnet.Channel", # noqa: UP037
args: list[str],
option_dict: dict[str, "Any"], # noqa: UP037
) -> None:
import os
import sys
outchannel = channel.gateway.newchannel()
sys.stdout = sys.stderr = outchannel.makefile("w")
channel.send(outchannel)
# prune sys.path to not contain relative paths
newpaths = []
for p in sys.path:
if p:
# Ignore path placeholders created for editable installs
if not os.path.isabs(p) and not p.endswith(".__path_hook__"):
p = os.path.abspath(p)
newpaths.append(p)
sys.path[:] = newpaths
# fullwidth, hasmarkup = channel.receive()
from pytest import Config
config = Config.fromdictargs(option_dict, list(args))
config.args = args
from xdist.looponfail import WorkerFailSession
WorkerFailSession(config, channel).main()
class WorkerFailSession:
def __init__(self, config: pytest.Config, channel: execnet.Channel) -> None:
self.config = config
self.channel = channel
self.recorded_failures: list[pytest.CollectReport | pytest.TestReport] = []
self.collection_failed = False
config.pluginmanager.register(self)
config.option.looponfail = False
config.option.usepdb = False
def DEBUG(self, *args: object) -> None:
if self.config.option.debug:
print(" ".join(map(str, args)))
@pytest.hookimpl
def pytest_collection(self, session: pytest.Session) -> bool:
self.session = session
self.trails = self.current_command
hook = self.session.ihook
try:
items = session.perform_collect(self.trails or None)
except pytest.UsageError:
items = session.perform_collect(None)
hook.pytest_collection_modifyitems(
session=session, config=session.config, items=items
)
hook.pytest_collection_finish(session=session)
return True
@pytest.hookimpl
def pytest_runtest_logreport(self, report: pytest.TestReport) -> None:
if report.failed:
self.recorded_failures.append(report)
@pytest.hookimpl
def pytest_collectreport(self, report: pytest.CollectReport) -> None:
if report.failed:
self.recorded_failures.append(report)
self.collection_failed = True
def main(self) -> None:
self.DEBUG("WORKER: received configuration, waiting for command trails")
try:
command = self.channel.receive()
except KeyboardInterrupt:
return # in the worker we can't do much about this
self.DEBUG("received", command)
self.current_command = command
self.config.hook.pytest_cmdline_main(config=self.config)
trails, failreports = [], []
for rep in self.recorded_failures:
trails.append(rep.nodeid)
loc = rep.longrepr
loc = str(getattr(loc, "reprcrash", loc))
failreports.append(loc)
result = (trails, failreports, self.collection_failed)
self.channel.send(result)
class StatRecorder:
def __init__(self, rootdirlist: Sequence[Path]) -> None:
self.rootdirlist = rootdirlist
self.statcache: dict[Path, os.stat_result] = {}
self.check() # snapshot state
def fil(self, p: Path) -> bool:
return p.is_file() and not p.name.startswith(".") and p.suffix != ".pyc"
def rec(self, p: Path) -> bool:
return not p.name.startswith(".") and p.exists()
def waitonchange(self, checkinterval: float = 1.0) -> None:
while 1:
changed = self.check()
if changed:
return
time.sleep(checkinterval)
def check(self, removepycfiles: bool = True) -> bool:
changed = False
newstat: dict[Path, os.stat_result] = {}
for rootdir in self.rootdirlist:
for path in visit_path(rootdir, filter=self.fil, recurse=self.rec):
oldstat = self.statcache.pop(path, None)
try:
curstat = path.stat()
except OSError:
if oldstat:
changed = True
else:
newstat[path] = curstat
if oldstat is not None:
if (
oldstat.st_mtime != curstat.st_mtime
or oldstat.st_size != curstat.st_size
):
changed = True
print("# MODIFIED", path)
if removepycfiles and path.suffix == ".py":
pycfile = path.with_suffix(".pyc")
if pycfile.is_file():
os.unlink(pycfile)
else:
changed = True
if self.statcache:
changed = True
self.statcache = newstat
return changed

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

@@ -0,0 +1,129 @@
"""
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
"""
from __future__ import annotations
from collections.abc import Sequence
import os
from typing import Any
from typing import TYPE_CHECKING
import execnet
import pytest
if TYPE_CHECKING:
from xdist.remote import Producer
from xdist.scheduler.protocol import Scheduling
from xdist.workermanage import WorkerController
@pytest.hookspec()
def pytest_xdist_setupnodes(
config: pytest.Config, specs: Sequence[execnet.XSpec]
) -> None:
"""Called before any remote node is set up."""
@pytest.hookspec()
def pytest_xdist_newgateway(gateway: execnet.Gateway) -> None:
"""Called on new raw gateway creation."""
@pytest.hookspec(
warn_on_impl=DeprecationWarning(
"rsync feature is deprecated and will be removed in pytest-xdist 4.0"
)
)
def pytest_xdist_rsyncstart(
source: str | os.PathLike[str],
gateways: Sequence[execnet.Gateway],
) -> None:
"""Called before rsyncing a directory to remote gateways takes place."""
@pytest.hookspec(
warn_on_impl=DeprecationWarning(
"rsync feature is deprecated and will be removed in pytest-xdist 4.0"
)
)
def pytest_xdist_rsyncfinish(
source: str | os.PathLike[str],
gateways: Sequence[execnet.Gateway],
) -> None:
"""Called after rsyncing a directory to remote gateways takes place."""
@pytest.hookspec(firstresult=True)
def pytest_xdist_getremotemodule() -> Any:
"""Called when creating remote node."""
@pytest.hookspec()
def pytest_configure_node(node: WorkerController) -> None:
"""Configure node information before it gets instantiated."""
@pytest.hookspec()
def pytest_testnodeready(node: WorkerController) -> None:
"""Test Node is ready to operate."""
@pytest.hookspec()
def pytest_testnodedown(node: WorkerController, error: object | None) -> None:
"""Test Node is down."""
@pytest.hookspec()
def pytest_xdist_node_collection_finished(
node: WorkerController, ids: Sequence[str]
) -> None:
"""Called by the controller node when a worker node finishes collecting."""
@pytest.hookspec(firstresult=True)
def pytest_xdist_make_scheduler(
config: pytest.Config, log: Producer
) -> Scheduling | None:
"""Return a node scheduler implementation."""
@pytest.hookspec(firstresult=True)
def pytest_xdist_auto_num_workers(config: pytest.Config) -> int:
"""
Return the number of workers to spawn when ``--numprocesses=auto`` is given in the
command-line.
.. versionadded:: 2.1
"""
raise NotImplementedError()
@pytest.hookspec(firstresult=True)
def pytest_handlecrashitem(
crashitem: str, report: pytest.TestReport, sched: Scheduling
) -> None:
"""
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
"""

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

@@ -0,0 +1,407 @@
from __future__ import annotations
import os
import sys
from typing import Literal
import uuid
import warnings
import pytest
_sys_path = list(sys.path) # freeze a copy of sys.path at interpreter startup
@pytest.hookimpl
def pytest_xdist_auto_num_workers(config: pytest.Config) -> int:
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: bool = 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() -> int:
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 # type: ignore[assignment]
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: str) -> int | Literal["auto", "logical"]:
if s in ("auto", "logical"):
return s # type: ignore[return-value]
elif s is not None:
return int(s)
@pytest.hookimpl
def pytest_addoption(parser: pytest.Parser) -> None:
# 'Help' formatting (same rules as pytest's):
# Start with capitalized letters.
# If a single phrase, do not end with period. If more than one phrase, all phrases end with periods.
# Use \n to separate logical lines.
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'.\n"
"With 'logical', attempt to detect logical CPU count (requires psutil, falls back to 'auto').\n"
"With 'auto', attempt to detect physical CPU count. If physical CPU count cannot be determined, "
"falls back to 1.\n"
"Forced to 0 (disabled) 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 "
"with 'auto' or 'logical'",
)
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",
"worksteal",
"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"
"worksteal: Split the test suite between available environments,"
" then re-balance when any worker runs out of tests.\n\n"
"(default) no: Run tests inprocess, don't distribute."
),
)
group.addoption(
"--loadscope-reorder",
dest="loadscopereorder",
action="store_true",
default=True,
help=(
"Pytest-xdist will default reorder tests by number of tests per scope "
"when used in conjunction with loadscope.\n"
"This option will enable loadscope reorder which will improve the "
"parallelism of the test suite.\n"
"However, the partial order of tests might not be retained.\n"
),
)
group.addoption(
"--no-loadscope-reorder",
dest="loadscopereorder",
action="store_false",
help=(
"Pytest-xdist will default reorder tests by number of tests per scope "
"when used in conjunction with loadscope.\n"
"This option will disable loadscope reorder, "
"and the partial order of tests can be retained.\n"
"This is useful when pytest-xdist is used together with "
"other plugins that specify tests in a specific order."
),
)
group.addoption(
"--tx",
dest="tx",
action="append",
default=[],
metavar="xspec",
help=(
"Add a test execution environment. Some examples:\n"
"--tx popen//python=python2.5 --tx socket=192.168.1.102:8888\n"
"--tx ssh=user@codespeak.net//chdir=testcache"
),
)
group.addoption(
"--px",
dest="px",
action="append",
default=[],
metavar="xspec",
help=(
"Add a proxy gateway to pass to test execution environments using `via`. Example:\n"
"--px id=my_proxy//socket=192.168.1.102:8888 --tx 5*popen//via=my_proxy"
),
)
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"
"If not provided, 'testrun_uid' is filled with a new unique string "
"on every test run."
),
)
group.addoption(
"--maxschedchunk",
action="store",
type=int,
help=(
"Maximum number of tests scheduled in one step for --dist=load.\n"
"Setting it to 1 will force pytest to send tests to workers one by "
"one - might be useful for a small number of slow tests.\n"
"Larger numbers will allow the scheduler to submit consecutive "
"chunks of tests to workers - allows reusing fixtures.\n"
"Due to implementation reasons, at least 2 tests are scheduled per "
"worker at the start. Only later tests can be scheduled one by one.\n"
"Unlimited if not set."
),
)
parser.addini(
"rsyncdirs",
"list of (relative) paths to be rsynced for remote distributed testing.",
type="paths",
)
parser.addini(
"rsyncignore",
"list of (relative) glob-style paths to be ignored for rsyncing.",
type="paths",
)
parser.addini(
"looponfailroots",
type="paths",
help="directories to check for changes. Default: current directory.",
)
# -------------------------------------------------------------------------
# distributed testing hooks
# -------------------------------------------------------------------------
@pytest.hookimpl
def pytest_addhooks(pluginmanager: pytest.PytestPluginManager) -> None:
from xdist import newhooks
pluginmanager.add_hookspecs(newhooks)
# -------------------------------------------------------------------------
# distributed testing initialization
# -------------------------------------------------------------------------
@pytest.hookimpl(trylast=True)
def pytest_configure(config: pytest.Config) -> None:
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 _is_distribution_mode(config):
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)
def _is_distribution_mode(config: pytest.Config) -> bool:
"""Whether distribution mode is on."""
return config.getoption("dist") != "no" and bool(config.getoption("tx"))
@pytest.hookimpl(tryfirst=True)
def pytest_cmdline_main(config: pytest.Config) -> None:
if config.option.distload:
config.option.dist = "load"
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.numprocesses == 0:
config.option.dist = "no"
config.option.tx = []
val = config.getvalue
if not val("collectonly") and _is_distribution_mode(config) and usepdb:
raise pytest.UsageError(
"--pdb is incompatible with distributing tests; try using -n0 or -nauto."
)
# -------------------------------------------------------------------------
# fixtures and API to easily know the role of current node
# -------------------------------------------------------------------------
def is_xdist_worker(
request_or_session: pytest.FixtureRequest | pytest.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: pytest.FixtureRequest | pytest.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: pytest.FixtureRequest | pytest.Session,
) -> str:
"""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"):
workerid: str = request_or_session.config.workerinput["workerid"]
return workerid
else:
# TODO: remove "master", ideally for a None
return "master"
@pytest.fixture(scope="session")
def worker_id(request: pytest.FixtureRequest) -> str:
"""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: pytest.FixtureRequest) -> str:
"""Return the unique id of the current test."""
if hasattr(request.config, "workerinput"):
testrunid: str = request.config.workerinput["testrunuid"]
return testrunid
else:
return uuid.uuid4().hex

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

@@ -0,0 +1,427 @@
"""
This module is executed in remote subprocesses and helps to
control a remote testing session and relay back information.
It assumes that 'py' is importable and does not have dependencies
on the rest of the xdist code. This means that the xdist-plugin
needs not to be installed in remote environments.
"""
from __future__ import annotations
import collections
from collections.abc import Generator
from collections.abc import Iterable
from collections.abc import Sequence
import contextlib
import enum
import os
import sys
import time
from typing import Any
from typing import Literal
from typing import TypedDict
from typing import Union
import warnings
from _pytest.config import _prepareconfig
import execnet
import pytest
try:
from setproctitle import setproctitle
except ImportError:
def setproctitle(title: str) -> None:
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) -> None:
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: str) -> None:
try:
setproctitle(title)
except Exception:
# changing the process name is very optional, no errors please
pass
class Marker(enum.Enum):
SHUTDOWN = 0
class TestQueue:
"""A simple queue that can be inspected and modified while the lock is held via the ``lock()`` method."""
Item = Union[int, Literal[Marker.SHUTDOWN]]
def __init__(self, execmodel: execnet.gateway_base.ExecModel):
self._items: collections.deque[TestQueue.Item] = collections.deque()
self._lock = execmodel.RLock() # type: ignore[no-untyped-call]
self._has_items_event = execmodel.Event()
def get(self) -> Item:
while True:
with self.lock() as locked_items:
if locked_items:
return locked_items.popleft()
self._has_items_event.wait()
def put(self, item: Item) -> None:
with self.lock() as locked_items:
locked_items.append(item)
def replace(self, iterable: Iterable[Item]) -> None:
with self.lock():
self._items = collections.deque(iterable)
@contextlib.contextmanager
def lock(self) -> Generator[collections.deque[Item]]:
with self._lock:
try:
yield self._items
finally:
if self._items:
self._has_items_event.set()
else:
self._has_items_event.clear()
class WorkerInteractor:
def __init__(self, config: pytest.Config, channel: execnet.Channel) -> None:
self.config = config
workerinput: dict[str, Any] = config.workerinput # type: ignore[attr-defined]
self.workerid = workerinput.get("workerid", "?")
self.testrunuid = workerinput["testrunuid"]
self.log = Producer(f"worker-{self.workerid}", enabled=config.option.debug)
self.channel = channel
self.torun = TestQueue(self.channel.gateway.execmodel)
self.nextitem_index: int | None | Literal[Marker.SHUTDOWN] = None
config.pluginmanager.register(self)
def sendevent(self, name: str, **kwargs: object) -> None:
self.log("sending", name, kwargs)
self.channel.send((name, kwargs))
@pytest.hookimpl
def pytest_internalerror(self, excrepr: object) -> None:
formatted_error = str(excrepr)
for line in formatted_error.split("\n"):
self.log("IERROR>", line)
interactor.sendevent("internal_error", formatted_error=formatted_error)
@pytest.hookimpl
def pytest_sessionstart(self, session: pytest.Session) -> None:
self.session = session
workerinfo = getinfodict()
self.sendevent("workerready", workerinfo=workerinfo)
@pytest.hookimpl(hookwrapper=True)
def pytest_sessionfinish(self, exitstatus: int) -> Generator[None, object, None]:
workeroutput: dict[str, Any] = self.config.workeroutput # type: ignore[attr-defined]
# in pytest 5.0+, exitstatus is an IntEnum object
workeroutput["exitstatus"] = int(exitstatus)
workeroutput["shouldfail"] = self.session.shouldfail
workeroutput["shouldstop"] = self.session.shouldstop
yield
self.sendevent("workerfinished", workeroutput=workeroutput)
@pytest.hookimpl
def pytest_collection(self) -> None:
self.sendevent("collectionstart")
def handle_command(
self, command: tuple[str, dict[str, Any]] | Literal[Marker.SHUTDOWN]
) -> None:
if command is Marker.SHUTDOWN:
self.torun.put(Marker.SHUTDOWN)
return
name, kwargs = command
self.log("received command", name, kwargs)
if name == "runtests":
for i in kwargs["indices"]:
self.torun.put(i)
elif name == "runtests_all":
for i in range(len(self.session.items)):
self.torun.put(i)
elif name == "shutdown":
self.torun.put(Marker.SHUTDOWN)
elif name == "steal":
self.steal(kwargs["indices"])
def steal(self, indices: Sequence[int]) -> None:
"""
Remove tests from the queue.
Removes either all requested tests, or none, if some of these tests
are not in the queue (for example, if they were processed already).
:param indices: indices of the tests to remove.
"""
requested_set = set(indices)
with self.torun.lock() as locked_queue:
stolen = list(item for item in locked_queue if item in requested_set)
# Stealing only if all requested tests are still pending
if len(stolen) == len(requested_set):
self.torun.replace(
item for item in locked_queue if item not in requested_set
)
else:
stolen = []
self.sendevent("unscheduled", indices=stolen)
@pytest.hookimpl
def pytest_runtestloop(self, session: pytest.Session) -> bool:
self.log("entering main loop")
self.channel.setcallback(self.handle_command, endmarker=Marker.SHUTDOWN)
self.nextitem_index = self.torun.get()
while self.nextitem_index is not Marker.SHUTDOWN:
self.run_one_test()
if session.shouldfail or session.shouldstop:
break
return True
def run_one_test(self) -> None:
assert isinstance(self.nextitem_index, int)
self.item_index = self.nextitem_index
self.nextitem_index = self.torun.get()
items = self.session.items
item = items[self.item_index]
if self.nextitem_index is Marker.SHUTDOWN:
nextitem = None
else:
assert self.nextitem_index is not None
nextitem = items[self.nextitem_index]
worker_title("[pytest-xdist running] %s" % item.nodeid)
start = time.perf_counter()
self.config.hook.pytest_runtest_protocol(item=item, nextitem=nextitem)
duration = time.perf_counter() - start
worker_title("[pytest-xdist idle]")
self.sendevent(
"runtest_protocol_complete", item_index=self.item_index, duration=duration
)
def pytest_collection_modifyitems(
self,
config: pytest.Config,
items: list[pytest.Item],
) -> None:
# add the group name to nodeid as suffix if --dist=loadgroup
if config.getvalue("loadgroup"):
for item in items:
gnames: set[str] = set()
for mark in item.iter_markers("xdist_group"):
name = (
mark.args[0]
if len(mark.args) > 0
else mark.kwargs.get("name", "default")
)
gnames.add(str(name))
if not gnames:
continue
item._nodeid = f"{item.nodeid}@{'_'.join(sorted(gnames))}"
@pytest.hookimpl
def pytest_collection_finish(self, session: pytest.Session) -> None:
self.sendevent(
"collectionfinish",
topdir=str(self.config.rootpath),
ids=[item.nodeid for item in session.items],
)
@pytest.hookimpl
def pytest_runtest_logstart(
self,
nodeid: str,
location: tuple[str, int | None, str],
) -> None:
self.sendevent("logstart", nodeid=nodeid, location=location)
@pytest.hookimpl
def pytest_runtest_logfinish(
self,
nodeid: str,
location: tuple[str, int | None, str],
) -> None:
self.sendevent("logfinish", nodeid=nodeid, location=location)
@pytest.hookimpl
def pytest_runtest_logreport(self, report: pytest.TestReport) -> None:
data = self.config.hook.pytest_report_to_serializable(
config=self.config, report=report
)
data["item_index"] = self.item_index
data["worker_id"] = self.workerid
data["testrun_uid"] = self.testrunuid
assert self.session.items[self.item_index].nodeid == report.nodeid
self.sendevent("testreport", data=data)
@pytest.hookimpl
def pytest_collectreport(self, report: pytest.CollectReport) -> None:
# send only reports that have not passed to controller as optimization (#330)
if not report.passed:
data = self.config.hook.pytest_report_to_serializable(
config=self.config, report=report
)
self.sendevent("collectreport", data=data)
@pytest.hookimpl
def pytest_warning_recorded(
self,
warning_message: warnings.WarningMessage,
when: str,
nodeid: str,
location: tuple[str, int, str] | None,
) -> None:
self.sendevent(
"warning_recorded",
warning_message_data=serialize_warning_message(warning_message),
when=when,
nodeid=nodeid,
location=location,
)
def serialize_warning_message(
warning_message: warnings.WarningMessage,
) -> dict[str, Any]:
if isinstance(warning_message.message, Warning):
message_module = type(warning_message.message).__module__
message_class_name = type(warning_message.message).__name__
message_str = str(warning_message.message)
# check now if we can serialize the warning arguments (#349)
# if not, we will just use the exception message on the controller node
try:
execnet.dumps(warning_message.message.args)
except execnet.DumpError:
message_args = None
else:
message_args = warning_message.message.args
else:
message_str = warning_message.message
message_module = None
message_class_name = None
message_args = None
if warning_message.category:
category_module = warning_message.category.__module__
category_class_name = warning_message.category.__name__
else:
category_module = None
category_class_name = None
result = {
"message_str": message_str,
"message_module": message_module,
"message_class_name": message_class_name,
"message_args": message_args,
"category_module": category_module,
"category_class_name": category_class_name,
}
# access private _WARNING_DETAILS because the attributes vary between Python versions
for attr_name in warning_message._WARNING_DETAILS: # type: ignore[attr-defined]
if attr_name in ("message", "category"):
continue
attr = getattr(warning_message, attr_name)
# Check if we can serialize the warning detail, marking `None` otherwise
# Note that we need to define the attr (even as `None`) to allow deserializing
try:
execnet.dumps(attr)
except execnet.DumpError:
result[attr_name] = repr(attr)
else:
result[attr_name] = attr
return result
class WorkerInfo(TypedDict):
version: str
version_info: tuple[int, int, int, str, int]
sysplatform: str
platform: str
executable: str
cwd: str
id: str
spec: execnet.XSpec
def getinfodict() -> WorkerInfo:
import platform
return dict(
version=sys.version,
version_info=tuple(sys.version_info), # type: ignore[typeddict-item]
sysplatform=sys.platform,
platform=platform.platform(),
executable=sys.executable,
cwd=os.getcwd(),
)
def setup_config(config: pytest.Config, basetemp: str | None) -> None:
config.option.loadgroup = config.getvalue("dist") == "loadgroup"
config.option.looponfail = False
config.option.usepdb = False
config.option.dist = "no"
config.option.distload = False
config.option.numprocesses = None
config.option.maxprocesses = None
config.option.basetemp = basetemp
if __name__ == "__channelexec__":
channel: execnet.Channel = channel # type: ignore[name-defined] # noqa: F821, PLW0127
workerinput, args, option_dict, change_sys_path = channel.receive() # type: ignore[name-defined]
if change_sys_path is None:
importpath = os.getcwd()
sys.path.insert(0, importpath)
os.environ["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_COUNT"] = str(workerinput["workercount"])
config = _prepareconfig(args, None)
setup_config(config, option_dict.get("basetemp"))
config._parser.prog = os.path.basename(workerinput["mainargv"][0])
config.workerinput = workerinput # type: ignore[attr-defined]
config.workeroutput = {} # type: ignore[attr-defined]
interactor = WorkerInteractor(config, channel) # type: ignore[name-defined]
config.hook.pytest_cmdline_main(config=config)

30
src/xdist/report.py Normal file
View File

@@ -0,0 +1,30 @@
from __future__ import annotations
from collections.abc import Sequence
from difflib import unified_diff
def report_collection_diff(
from_collection: Sequence[str],
to_collection: Sequence[str],
from_id: str,
to_id: str,
) -> str | None:
"""Report the collected test difference between two nodes.
:returns: detailed message describing the difference between the given
collections, or None if they are equal.
"""
if from_collection == to_collection:
return None
diff = unified_diff(from_collection, to_collection, fromfile=from_id, tofile=to_id)
error_message = (
"Different tests were collected between {from_id} and {to_id}. "
"The difference is:\n"
"{diff}\n"
"To see why this happens see 'Known limitations' in documentation "
"for pytest-xdist"
).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"))
return msg

View File

@@ -0,0 +1,7 @@
from xdist.scheduler.each import EachScheduling as EachScheduling
from xdist.scheduler.load import LoadScheduling as LoadScheduling
from xdist.scheduler.loadfile import LoadFileScheduling as LoadFileScheduling
from xdist.scheduler.loadgroup import LoadGroupScheduling as LoadGroupScheduling
from xdist.scheduler.loadscope import LoadScopeScheduling as LoadScopeScheduling
from xdist.scheduler.protocol import Scheduling as Scheduling
from xdist.scheduler.worksteal import WorkStealingScheduling as WorkStealingScheduling

152
src/xdist/scheduler/each.py Normal file
View File

@@ -0,0 +1,152 @@
from __future__ import annotations
from collections.abc import Sequence
import pytest
from xdist.remote import Producer
from xdist.report import report_collection_diff
from xdist.workermanage import parse_tx_spec_config
from xdist.workermanage import WorkerController
class EachScheduling:
"""Implement scheduling of test items on all nodes.
If a node gets added after the test run is started then it is
assumed to replace a node which got removed before it finished
its collection. In this case it will only be used if a node
with the same spec got removed earlier.
Any nodes added after the run is started will only get items
assigned if a node with a matching spec was removed before it
finished all its pending items. The new node will then be
assigned the remaining items from the removed node.
"""
def __init__(self, config: pytest.Config, log: Producer | None = None) -> None:
self.config = config
self.numnodes = len(parse_tx_spec_config(config))
self.node2collection: dict[WorkerController, list[str]] = {}
self.node2pending: dict[WorkerController, list[int]] = {}
self._started: list[WorkerController] = []
self._removed2pending: dict[WorkerController, list[int]] = {}
if log is None:
self.log = Producer("eachsched")
else:
self.log = log.eachsched
self.collection_is_completed = False
@property
def nodes(self) -> list[WorkerController]:
"""A list of all nodes in the scheduler."""
return list(self.node2pending.keys())
@property
def tests_finished(self) -> bool:
if not self.collection_is_completed:
return False
if self._removed2pending:
return False
for pending in self.node2pending.values():
if len(pending) >= 2:
return False
return True
@property
def has_pending(self) -> bool:
"""Return True if there are pending test items.
This indicates that collection has finished and nodes are
still processing test items, so this can be thought of as
"the scheduler is active".
"""
for pending in self.node2pending.values():
if pending:
return True
return False
def add_node(self, node: WorkerController) -> None:
assert node not in self.node2pending
self.node2pending[node] = []
def add_node_collection(
self, node: WorkerController, collection: Sequence[str]
) -> None:
"""Add the collected test items from a node.
Collection is complete once all nodes have submitted their
collection. In this case its pending list is set to an empty
list. When the collection is already completed this
submission is from a node which was restarted to replace a
dead node. In this case we already assign the pending items
here. In either case ``.schedule()`` will instruct the
node to start running the required tests.
"""
assert node in self.node2pending
if not self.collection_is_completed:
self.node2collection[node] = list(collection)
self.node2pending[node] = []
if len(self.node2collection) >= self.numnodes:
self.collection_is_completed = True
elif self._removed2pending:
for deadnode in self._removed2pending:
if deadnode.gateway.spec == node.gateway.spec:
dead_collection = self.node2collection[deadnode]
if collection != dead_collection:
msg = report_collection_diff(
dead_collection,
collection,
deadnode.gateway.id,
node.gateway.id,
)
self.log(msg)
return
pending = self._removed2pending.pop(deadnode)
self.node2pending[node] = pending
break
def mark_test_complete(
self, node: WorkerController, item_index: int, duration: float = 0
) -> None:
self.node2pending[node].remove(item_index)
def mark_test_pending(self, item: str) -> None:
raise NotImplementedError()
def remove_pending_tests_from_node(
self,
node: WorkerController,
indices: Sequence[int],
) -> None:
raise NotImplementedError()
def remove_node(self, node: WorkerController) -> str | None:
# KeyError if we didn't get an add_node() yet
pending = self.node2pending.pop(node)
if not pending:
return None
crashitem = self.node2collection[node][pending.pop(0)]
if pending:
self._removed2pending[node] = pending
return crashitem
def schedule(self) -> None:
"""Schedule the test items on the nodes.
If the node's pending list is empty it is a new node which
needs to run all the tests. If the pending list is already
populated (by ``.add_node_collection()``) then it replaces a
dead node and we only need to run those tests.
"""
assert self.collection_is_completed
for node, pending in self.node2pending.items():
if node in self._started:
continue
if not pending:
pending[:] = range(len(self.node2collection[node]))
node.send_runtest_all()
node.shutdown()
else:
node.send_runtest_some(pending)
self._started.append(node)

335
src/xdist/scheduler/load.py Normal file
View File

@@ -0,0 +1,335 @@
from __future__ import annotations
from collections.abc import Sequence
from itertools import cycle
import pytest
from xdist.remote import Producer
from xdist.report import report_collection_diff
from xdist.workermanage import parse_tx_spec_config
from xdist.workermanage import WorkerController
class LoadScheduling:
"""Implement load scheduling across nodes.
This distributes the tests collected across all nodes so each test
is run just once. All nodes collect and submit the test suite and
when all collections are received it is verified they are
identical collections. Then the collection gets divided up in
chunks and chunks get submitted to nodes. Whenever a node finishes
an item, it calls ``.mark_test_complete()`` which will trigger the
scheduler to assign more tests if the number of pending tests for
the node falls below a low-watermark.
When created, ``numnodes`` defines how many nodes are expected to
submit a collection. This is used to know when all nodes have
finished collection or how large the chunks need to be created.
Attributes::
:numnodes: The expected number of nodes taking part. The actual
number of nodes will vary during the scheduler's lifetime as
nodes are added by the DSession as they are brought up and
removed either because of a dead node or normal shutdown. This
number is primarily used to know when the initial collection is
completed.
:node2collection: Map of nodes and their test collection. All
collections should always be identical.
:node2pending: Map of nodes and the indices of their pending
tests. The indices are an index into ``.pending`` (which is
identical to their own collection stored in
``.node2collection``).
:collection: The one collection once it is validated to be
identical between all the nodes. It is initialised to None
until ``.schedule()`` is called.
:pending: List of indices of globally pending tests. These are
tests which have not yet been allocated to a chunk for a node
to process.
:log: A py.log.Producer instance.
:config: Config object, used for handling hooks.
"""
def __init__(self, config: pytest.Config, log: Producer | None = None) -> None:
self.numnodes = len(parse_tx_spec_config(config))
self.node2collection: dict[WorkerController, list[str]] = {}
self.node2pending: dict[WorkerController, list[int]] = {}
self.pending: list[int] = []
self.collection: list[str] | None = None
if log is None:
self.log = Producer("loadsched")
else:
self.log = log.loadsched
self.config = config
self.maxschedchunk = self.config.getoption("maxschedchunk")
@property
def nodes(self) -> list[WorkerController]:
"""A list of all nodes in the scheduler."""
return list(self.node2pending.keys())
@property
def collection_is_completed(self) -> bool:
"""Boolean indication initial test collection is complete.
This is a boolean indicating all initial participating nodes
have finished collection. The required number of initial
nodes is defined by ``.numnodes``.
"""
return len(self.node2collection) >= self.numnodes
@property
def tests_finished(self) -> bool:
"""Return True if all tests have been executed by the nodes."""
if not self.collection_is_completed:
return False
if self.pending:
return False
for pending in self.node2pending.values():
if len(pending) >= 2:
return False
return True
@property
def has_pending(self) -> bool:
"""Return True if there are pending test items.
This indicates that collection has finished and nodes are
still processing test items, so this can be thought of as
"the scheduler is active".
"""
if self.pending:
return True
for pending in self.node2pending.values():
if pending:
return True
return False
def add_node(self, node: WorkerController) -> None:
"""Add a new node to the scheduler.
From now on the node will be allocated chunks of tests to
execute.
Called by the ``DSession.worker_workerready`` hook when it
successfully bootstraps a new node.
"""
assert node not in self.node2pending
self.node2pending[node] = []
def add_node_collection(
self, node: WorkerController, collection: Sequence[str]
) -> None:
"""Add the collected test items from a node.
The collection is stored in the ``.node2collection`` map.
Called by the ``DSession.worker_collectionfinish`` hook.
"""
assert node in self.node2pending
if self.collection_is_completed:
# A new node has been added later, perhaps an original one died.
# .schedule() should have
# been called by now
assert self.collection
if collection != self.collection:
other_node = next(iter(self.node2collection.keys()))
msg = report_collection_diff(
self.collection, collection, other_node.gateway.id, node.gateway.id
)
self.log(msg)
return
self.node2collection[node] = list(collection)
def mark_test_complete(
self, node: WorkerController, item_index: int, duration: float = 0
) -> None:
"""Mark test item as completed by node.
The duration it took to execute the item is used as a hint to
the scheduler.
This is called by the ``DSession.worker_testreport`` hook.
"""
self.node2pending[node].remove(item_index)
self.check_schedule(node, duration=duration)
def mark_test_pending(self, item: str) -> None:
assert self.collection is not None
self.pending.insert(
0,
self.collection.index(item),
)
for node in self.node2pending:
self.check_schedule(node)
def remove_pending_tests_from_node(
self,
node: WorkerController,
indices: Sequence[int],
) -> None:
raise NotImplementedError()
def check_schedule(self, node: WorkerController, duration: float = 0) -> None:
"""Maybe schedule new items on the node.
If there are any globally pending nodes left then this will
check if the given node should be given any more tests. The
``duration`` of the last test is optionally used as a
heuristic to influence how many tests the node is assigned.
"""
if node.shutting_down:
return
if self.pending:
# how many nodes do we have?
num_nodes = len(self.node2pending)
# if our node goes below a heuristic minimum, fill it out to
# heuristic maximum
items_per_node_min = max(2, len(self.pending) // num_nodes // 4)
items_per_node_max = max(2, len(self.pending) // num_nodes // 2)
node_pending = self.node2pending[node]
if len(node_pending) < items_per_node_min:
if duration >= 0.1 and len(node_pending) >= 2:
# seems the node is doing long-running tests
# and has enough items to continue
# so let's rather wait with sending new items
return
num_send = items_per_node_max - len(node_pending)
# keep at least 2 tests pending even if --maxschedchunk=1
maxschedchunk = max(2 - len(node_pending), self.maxschedchunk)
self._send_tests(node, min(num_send, maxschedchunk))
else:
node.shutdown()
self.log("num items waiting for node:", len(self.pending))
def remove_node(self, node: WorkerController) -> str | None:
"""Remove a node from the scheduler.
This should be called either when the node crashed or at
shutdown time. In the former case any pending items assigned
to the node will be re-scheduled. Called by the
``DSession.worker_workerfinished`` and
``DSession.worker_errordown`` hooks.
Return the item which was being executing while the node
crashed or None if the node has no more pending items.
"""
pending = self.node2pending.pop(node)
if not pending:
return None
# The node crashed, reassing pending items
assert self.collection is not None
crashitem = self.collection[pending.pop(0)]
self.pending.extend(pending)
for node in self.node2pending:
self.check_schedule(node)
return crashitem
def schedule(self) -> None:
"""Initiate distribution of the test collection.
Initiate scheduling of the items across the nodes. If this
gets called again later it behaves the same as calling
``.check_schedule()`` on all nodes so that newly added nodes
will start to be used.
This is called by the ``DSession.worker_collectionfinish`` hook
if ``.collection_is_completed`` is True.
"""
assert self.collection_is_completed
# Initial distribution already happened, reschedule on all nodes
if self.collection is not None:
for node in self.nodes:
self.check_schedule(node)
return
# XXX allow nodes to have different collections
if not self._check_nodes_have_same_collection():
self.log("**Different tests collected, aborting run**")
return
# Collections are identical, create the index of pending items.
self.collection = next(iter(self.node2collection.values()))
self.pending[:] = range(len(self.collection))
if not self.collection:
return
if self.maxschedchunk is None:
self.maxschedchunk = len(self.collection)
# 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
# shutdown signals and get all nodes working.
if len(self.pending) < 2 * len(self.nodes):
# Distribute tests round-robin. Try to load all nodes if there are
# enough tests. The other branch tries sends at least 2 tests
# to each node - which is suboptimal when you have less than
# 2 * len(nodes) tests.
nodes = cycle(self.nodes)
for _ in range(len(self.pending)):
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 = min(items_per_node // 4, self.maxschedchunk)
node_chunksize = max(node_chunksize, 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:
# initial distribution sent all tests, start node shutdown
for node in self.nodes:
node.shutdown()
def _send_tests(self, node: WorkerController, num: int) -> None:
tests_per_node = self.pending[:num]
if tests_per_node:
del self.pending[:num]
self.node2pending[node].extend(tests_per_node)
node.send_runtest_some(tests_per_node)
def _check_nodes_have_same_collection(self) -> bool:
"""Return True if all nodes have collected the same items.
If collections differ, this method returns False while logging
the collection differences and posting collection errors to
pytest_collectreport hook.
"""
node_collection_items = list(self.node2collection.items())
first_node, col = node_collection_items[0]
same_collection = True
for node, collection in node_collection_items[1:]:
msg = report_collection_diff(
col, collection, first_node.gateway.id, node.gateway.id
)
if msg:
same_collection = False
self.log(msg)
if self.config is not None:
rep = pytest.CollectReport(
nodeid=node.gateway.id,
outcome="failed",
longrepr=msg,
result=[],
)
self.config.hook.pytest_collectreport(report=rep)
return same_collection

View File

@@ -0,0 +1,60 @@
from __future__ import annotations
import pytest
from xdist.remote import Producer
from .loadscope import LoadScopeScheduling
class LoadFileScheduling(LoadScopeScheduling):
"""Implement load scheduling across nodes, but grouping test test file.
This distributes the tests collected across all nodes so each test is run
just once. All nodes collect and submit the list of tests and when all
collections are received it is verified they are identical collections.
Then the collection gets divided up in work units, grouped by test file,
and those work units get submitted to nodes. Whenever a node finishes an
item, it calls ``.mark_test_complete()`` which will trigger the scheduler
to assign more work units if the number of pending tests for the node falls
below a low-watermark.
When created, ``numnodes`` defines how many nodes are expected to submit a
collection. This is used to know when all nodes have finished collection.
This class behaves very much like LoadScopeScheduling, but with a file-level scope.
"""
def __init__(self, config: pytest.Config, log: Producer | None = None) -> None:
super().__init__(config, log)
if log is None:
self.log = Producer("loadfilesched")
else:
self.log = log.loadfilesched
def _split_scope(self, nodeid: str) -> str:
"""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.
This function will group tests with the scope determined by splitting
the first ``::`` from the left. That is, test will be grouped in a
single work unit when they reside in the same file.
In the above example, scopes will be::
.. code-block:: text
example/loadsuite/test/test_beta.py
example/loadsuite/test/test_delta.py
example/loadsuite/epsilon/__init__.py
"""
return nodeid.split("::", 1)[0]

View File

@@ -0,0 +1,59 @@
from __future__ import annotations
import pytest
from xdist.remote import Producer
from .loadscope import LoadScopeScheduling
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: pytest.Config, log: Producer | None = None) -> None:
super().__init__(config, log)
if log is None:
self.log = Producer("loadgroupsched")
else:
self.log = log.loadgroupsched
def _split_scope(self, nodeid: str) -> str:
"""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

@@ -0,0 +1,438 @@
from __future__ import annotations
from collections import OrderedDict
from collections.abc import Sequence
from typing import NoReturn
import pytest
from xdist.remote import Producer
from xdist.report import report_collection_diff
from xdist.workermanage import parse_tx_spec_config
from xdist.workermanage import WorkerController
class LoadScopeScheduling:
"""Implement load scheduling across nodes, but grouping test by scope.
This distributes the tests collected across all nodes so each test is run
just once. All nodes collect and submit the list of tests and when all
collections are received it is verified they are identical collections.
Then the collection gets divided up in work units, grouped by test scope,
and those work units get submitted to nodes. Whenever a node finishes an
item, it calls ``.mark_test_complete()`` which will trigger the scheduler
to assign more work units if the number of pending tests for the node falls
below a low-watermark.
When created, ``numnodes`` defines how many nodes are expected to submit a
collection. This is used to know when all nodes have finished collection.
Attributes::
:numnodes: The expected number of nodes taking part. The actual number of
nodes will vary during the scheduler's lifetime as nodes are added by
the DSession as they are brought up and removed either because of a dead
node or normal shutdown. This number is primarily used to know when the
initial collection is completed.
:collection: The final list of tests collected by all nodes once it is
validated to be identical between all the nodes. It is initialised to
None until ``.schedule()`` is called.
:workqueue: Ordered dictionary that maps all available scopes with their
associated tests (nodeid). Nodeids are in turn associated with their
completion status. One entry of the workqueue is called a work unit.
In turn, a collection of work unit is called a workload.
::
workqueue = {
'<full>/<path>/<to>/test_module.py': {
'<full>/<path>/<to>/test_module.py::test_case1': False,
'<full>/<path>/<to>/test_module.py::test_case2': False,
(...)
},
(...)
}
:assigned_work: Ordered dictionary that maps worker nodes with their
assigned work units.
::
assigned_work = {
'<worker node A>': {
'<full>/<path>/<to>/test_module.py': {
'<full>/<path>/<to>/test_module.py::test_case1': False,
'<full>/<path>/<to>/test_module.py::test_case2': False,
(...)
},
(...)
},
(...)
}
:registered_collections: Ordered dictionary that maps worker nodes with
their collection of tests gathered during test discovery.
::
registered_collections = {
'<worker node A>': [
'<full>/<path>/<to>/test_module.py::test_case1',
'<full>/<path>/<to>/test_module.py::test_case2',
],
(...)
}
:log: A py.log.Producer instance.
:config: Config object, used for handling hooks.
"""
def __init__(self, config: pytest.Config, log: Producer | None = None) -> None:
self.numnodes = len(parse_tx_spec_config(config))
self.collection: list[str] | None = None
self.workqueue: OrderedDict[str, dict[str, bool]] = OrderedDict()
self.assigned_work: dict[WorkerController, dict[str, dict[str, bool]]] = {}
self.registered_collections: dict[WorkerController, list[str]] = {}
if log is None:
self.log = Producer("loadscopesched")
else:
self.log = log.loadscopesched
self.config = config
@property
def nodes(self) -> list[WorkerController]:
"""A list of all active nodes in the scheduler."""
return list(self.assigned_work.keys())
@property
def collection_is_completed(self) -> bool:
"""Boolean indication initial test collection is complete.
This is a boolean indicating all initial participating nodes have
finished collection. The required number of initial nodes is defined
by ``.numnodes``.
"""
return len(self.registered_collections) >= self.numnodes
@property
def tests_finished(self) -> bool:
"""Return True if all tests have been executed by the nodes."""
if not self.collection_is_completed:
return False
if self.workqueue:
return False
for assigned_unit in self.assigned_work.values():
if self._pending_of(assigned_unit) >= 2:
return False
return True
@property
def has_pending(self) -> bool:
"""Return True if there are pending test items.
This indicates that collection has finished and nodes are still
processing test items, so this can be thought of as
"the scheduler is active".
"""
if self.workqueue:
return True
for assigned_unit in self.assigned_work.values():
if self._pending_of(assigned_unit) > 0:
return True
return False
def add_node(self, node: WorkerController) -> None:
"""Add a new node to the scheduler.
From now on the node will be assigned work units to be executed.
Called by the ``DSession.worker_workerready`` hook when it successfully
bootstraps a new node.
"""
assert node not in self.assigned_work
self.assigned_work[node] = {}
def remove_node(self, node: WorkerController) -> str | None:
"""Remove a node from the scheduler.
This should be called either when the node crashed or at shutdown time.
In the former case any pending items assigned to the node will be
re-scheduled.
Called by the hooks:
- ``DSession.worker_workerfinished``.
- ``DSession.worker_errordown``.
Return the item being executed while the node crashed or None if the
node has no more pending items.
"""
workload = self.assigned_work.pop(node)
if not self._pending_of(workload):
return None
# The node crashed, identify test that crashed
for work_unit in workload.values():
for nodeid, completed in work_unit.items():
if not completed:
crashitem = nodeid
break
else:
continue
break
else:
raise RuntimeError(
"Unable to identify crashitem on a workload with pending items"
)
# Made uncompleted work unit available again
self.workqueue.update(workload)
for node in self.assigned_work:
self._reschedule(node)
return crashitem
def add_node_collection(
self, node: WorkerController, collection: Sequence[str]
) -> None:
"""Add the collected test items from a node.
The collection is stored in the ``.registered_collections`` dictionary.
Called by the hook:
- ``DSession.worker_collectionfinish``.
"""
# Check that add_node() was called on the node before
assert node in self.assigned_work
# A new node has been added later, perhaps an original one died.
if self.collection_is_completed:
# Assert that .schedule() should have been called by now
assert self.collection
# Check that the new collection matches the official collection
if collection != self.collection:
other_node = next(iter(self.registered_collections.keys()))
msg = report_collection_diff(
self.collection, collection, other_node.gateway.id, node.gateway.id
)
self.log(msg)
return
self.registered_collections[node] = list(collection)
def mark_test_complete(
self, node: WorkerController, item_index: int, duration: float = 0
) -> None:
"""Mark test item as completed by node.
Called by the hook:
- ``DSession.worker_testreport``.
"""
nodeid = self.registered_collections[node][item_index]
scope = self._split_scope(nodeid)
self.assigned_work[node][scope][nodeid] = True
self._reschedule(node)
def mark_test_pending(self, item: str) -> NoReturn:
raise NotImplementedError()
def remove_pending_tests_from_node(
self,
node: WorkerController,
indices: Sequence[int],
) -> None:
raise NotImplementedError()
def _assign_work_unit(self, node: WorkerController) -> None:
"""Assign a work unit to a node."""
assert self.workqueue
# Grab a unit of work
scope, work_unit = self.workqueue.popitem(last=False)
# Keep track of the assigned work
assigned_to_node = self.assigned_work.setdefault(node, {})
assigned_to_node[scope] = work_unit
# Ask the node to execute the workload
worker_collection = self.registered_collections[node]
nodeids_indexes = [
worker_collection.index(nodeid)
for nodeid, completed in work_unit.items()
if not completed
]
node.send_runtest_some(nodeids_indexes)
def _split_scope(self, nodeid: str) -> str:
"""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.
This function will group tests with the scope determined by splitting
the first ``::`` from the right. That is, classes will be grouped in a
single work unit, and functions from a test module will be grouped by
their module. In the above example, scopes will be::
example/loadsuite/test/test_beta.py
example/loadsuite/test/test_delta.py::Delta1
example/loadsuite/epsilon/__init__.py
"""
return nodeid.rsplit("::", 1)[0]
def _pending_of(self, workload: dict[str, dict[str, bool]]) -> int:
"""Return the number of pending tests in a workload."""
pending = sum(list(scope.values()).count(False) for scope in workload.values())
return pending
def _reschedule(self, node: WorkerController) -> None:
"""Maybe schedule new items on the node.
If there are any globally pending work units left then this will check
if the given node should be given any more tests.
"""
# Do not add more work to a node shutting down
if node.shutting_down:
return
# Check that more work is available
if not self.workqueue:
node.shutdown()
return
self.log("Number of units waiting for node:", len(self.workqueue))
# Check that the node is almost depleted of work
# 2: Heuristic of minimum tests to enqueue more work
if self._pending_of(self.assigned_work[node]) > 2:
return
# Pop one unit of work and assign it
self._assign_work_unit(node)
def schedule(self) -> None:
"""Initiate distribution of the test collection.
Initiate scheduling of the items across the nodes. If this gets called
again later it behaves the same as calling ``._reschedule()`` on all
nodes so that newly added nodes will start to be used.
If ``.collection_is_completed`` is True, this is called by the hook:
- ``DSession.worker_collectionfinish``.
"""
assert self.collection_is_completed
# Initial distribution already happened, reschedule on all nodes
if self.collection is not None:
for node in self.nodes:
self._reschedule(node)
return
# Check that all nodes collected the same tests
if not self._check_nodes_have_same_collection():
self.log("**Different tests collected, aborting run**")
return
# Collections are identical, create the final list of items
self.collection = list(next(iter(self.registered_collections.values())))
if not self.collection:
return
# Determine chunks of work (scopes)
unsorted_workqueue: dict[str, dict[str, bool]] = {}
for nodeid in self.collection:
scope = self._split_scope(nodeid)
work_unit = unsorted_workqueue.setdefault(scope, {})
work_unit[nodeid] = False
if self.config.option.loadscopereorder:
# Insert tests scopes into work queue ordered by number of tests.
for scope, nodeids in sorted(
unsorted_workqueue.items(), key=lambda item: -len(item[1])
):
self.workqueue[scope] = nodeids
else:
for scope, nodeids in unsorted_workqueue.items():
self.workqueue[scope] = nodeids
# Avoid having more workers than work
extra_nodes = len(self.nodes) - len(self.workqueue)
if extra_nodes > 0:
self.log(f"Shutting down {extra_nodes} nodes")
for _ in range(extra_nodes):
unused_node, assigned = self.assigned_work.popitem()
self.log(f"Shutting down unused node {unused_node}")
unused_node.shutdown()
# Assign initial workload
for node in self.nodes:
self._assign_work_unit(node)
# Ensure nodes start with at least two work units if possible (#277)
for node in self.nodes:
self._reschedule(node)
# Initial distribution sent all tests, start node shutdown
if not self.workqueue:
for node in self.nodes:
node.shutdown()
def _check_nodes_have_same_collection(self) -> bool:
"""Return True if all nodes have collected the same items.
If collections differ, this method returns False while logging
the collection differences and posting collection errors to
pytest_collectreport hook.
"""
node_collection_items = list(self.registered_collections.items())
first_node, col = node_collection_items[0]
same_collection = True
for node, collection in node_collection_items[1:]:
msg = report_collection_diff(
col, collection, first_node.gateway.id, node.gateway.id
)
if not msg:
continue
same_collection = False
self.log(msg)
rep = pytest.CollectReport(
nodeid=node.gateway.id,
outcome="failed",
longrepr=msg,
result=[],
)
self.config.hook.pytest_collectreport(report=rep)
return same_collection

View File

@@ -0,0 +1,47 @@
from __future__ import annotations
from collections.abc import Sequence
from typing import Protocol
from xdist.workermanage import WorkerController
class Scheduling(Protocol):
@property
def nodes(self) -> list[WorkerController]: ...
@property
def collection_is_completed(self) -> bool: ...
@property
def tests_finished(self) -> bool: ...
@property
def has_pending(self) -> bool: ...
def add_node(self, node: WorkerController) -> None: ...
def add_node_collection(
self,
node: WorkerController,
collection: Sequence[str],
) -> None: ...
def mark_test_complete(
self,
node: WorkerController,
item_index: int,
duration: float = 0,
) -> None: ...
def mark_test_pending(self, item: str) -> None: ...
def remove_pending_tests_from_node(
self,
node: WorkerController,
indices: Sequence[int],
) -> None: ...
def remove_node(self, node: WorkerController) -> str | None: ...
def schedule(self) -> None: ...

View File

@@ -0,0 +1,345 @@
from __future__ import annotations
from collections.abc import Sequence
from typing import NamedTuple
import pytest
from xdist.remote import Producer
from xdist.report import report_collection_diff
from xdist.workermanage import parse_tx_spec_config
from xdist.workermanage import WorkerController
class NodePending(NamedTuple):
node: WorkerController
pending: list[int]
# Every worker needs at least 2 tests in queue - the current and the next one.
MIN_PENDING = 2
class WorkStealingScheduling:
"""Implement work-stealing scheduling.
Initially, tests are distributed evenly among all nodes.
When some node completes most of its assigned tests (when only one pending
test remains), an attempt is made to reassign ("steal") some tests from
other nodes to this node.
Attributes::
:numnodes: The expected number of nodes taking part. The actual
number of nodes will vary during the scheduler's lifetime as
nodes are added by the DSession as they are brought up and
removed either because of a dead node or normal shutdown. This
number is primarily used to know when the initial collection is
completed.
:node2collection: Map of nodes and their test collection. All
collections should always be identical.
:node2pending: Map of nodes and the indices of their pending
tests. The indices are an index into ``.pending`` (which is
identical to their own collection stored in
``.node2collection``).
:collection: The one collection once it is validated to be
identical between all the nodes. It is initialised to None
until ``.schedule()`` is called.
:pending: List of indices of globally pending tests. These are
tests which have not yet been allocated to a chunk for a node
to process.
:log: A py.log.Producer instance.
:config: Config object, used for handling hooks.
:steal_requested_from_node: The node to which the current "steal" request
was sent. ``None`` if there is no request in progress. Only one request
can be in progress at any time, the scheduler doesn't send multiple
simultaneous requests.
"""
def __init__(self, config: pytest.Config, log: Producer | None = None) -> None:
self.numnodes = len(parse_tx_spec_config(config))
self.node2collection: dict[WorkerController, list[str]] = {}
self.node2pending: dict[WorkerController, list[int]] = {}
self.pending: list[int] = []
self.collection: list[str] | None = None
if log is None:
self.log = Producer("workstealsched")
else:
self.log = log.workstealsched
self.config = config
self.steal_requested_from_node: WorkerController | None = None
@property
def nodes(self) -> list[WorkerController]:
"""A list of all nodes in the scheduler."""
return list(self.node2pending.keys())
@property
def collection_is_completed(self) -> bool:
"""Boolean indication initial test collection is complete.
This is a boolean indicating all initial participating nodes
have finished collection. The required number of initial
nodes is defined by ``.numnodes``.
"""
return len(self.node2collection) >= self.numnodes
@property
def tests_finished(self) -> bool:
"""Return True if all tests have been executed by the nodes."""
if not self.collection_is_completed:
return False
if self.pending:
return False
if self.steal_requested_from_node is not None:
return False
for pending in self.node2pending.values():
if len(pending) >= MIN_PENDING:
return False
return True
@property
def has_pending(self) -> bool:
"""Return True if there are pending test items.
This indicates that collection has finished and nodes are
still processing test items, so this can be thought of as
"the scheduler is active".
"""
if self.pending:
return True
for pending in self.node2pending.values():
if pending:
return True
return False
def add_node(self, node: WorkerController) -> None:
"""Add a new node to the scheduler.
From now on the node will be allocated chunks of tests to
execute.
Called by the ``DSession.worker_workerready`` hook when it
successfully bootstraps a new node.
"""
assert node not in self.node2pending
self.node2pending[node] = []
def add_node_collection(
self, node: WorkerController, collection: Sequence[str]
) -> None:
"""Add the collected test items from a node.
The collection is stored in the ``.node2collection`` map.
Called by the ``DSession.worker_collectionfinish`` hook.
"""
assert node in self.node2pending
if self.collection_is_completed:
# A new node has been added later, perhaps an original one died.
# .schedule() should have
# been called by now
assert self.collection
if collection != self.collection:
other_node = next(iter(self.node2collection.keys()))
msg = report_collection_diff(
self.collection, collection, other_node.gateway.id, node.gateway.id
)
self.log(msg)
return
self.node2collection[node] = list(collection)
def mark_test_complete(
self, node: WorkerController, item_index: int, duration: float | None = None
) -> None:
"""Mark test item as completed by node.
This is called by the ``DSession.worker_testreport`` hook.
"""
self.node2pending[node].remove(item_index)
self.check_schedule()
def mark_test_pending(self, item: str) -> None:
assert self.collection is not None
self.pending.insert(
0,
self.collection.index(item),
)
self.check_schedule()
def remove_pending_tests_from_node(
self,
node: WorkerController,
indices: Sequence[int],
) -> None:
"""Node returned some test indices back in response to 'steal' command.
This is called by ``DSession.worker_unscheduled``.
"""
assert node is self.steal_requested_from_node
self.steal_requested_from_node = None
indices_set = set(indices)
self.node2pending[node] = [
i for i in self.node2pending[node] if i not in indices_set
]
self.pending.extend(indices)
self.check_schedule()
def check_schedule(self) -> None:
"""Reschedule tests/perform load balancing."""
nodes_up = [
NodePending(node, pending)
for node, pending in self.node2pending.items()
if not node.shutting_down
]
def get_idle_nodes() -> list[WorkerController]:
return [node for node, pending in nodes_up if len(pending) < MIN_PENDING]
idle_nodes = get_idle_nodes()
if not idle_nodes:
return
if self.pending:
# Distribute pending tests evenly among idle nodes
for i, node in enumerate(idle_nodes):
nodes_remaining = len(idle_nodes) - i
num_send = len(self.pending) // nodes_remaining
self._send_tests(node, num_send)
idle_nodes = get_idle_nodes()
# No need to steal anything if all nodes have enough work to continue
if not idle_nodes:
return
# Only one active stealing request is allowed
if self.steal_requested_from_node is not None:
return
# Find the node that has the longest test queue
steal_from = max(
nodes_up, key=lambda node_pending: len(node_pending.pending), default=None
)
if steal_from is None:
num_steal = 0
else:
# Steal half of the test queue - but keep that node running too.
# If the node has 2 or less tests queued, stealing will fail
# anyway.
max_steal = max(0, len(steal_from.pending) - MIN_PENDING)
num_steal = min(len(steal_from.pending) // 2, max_steal)
if num_steal == 0:
# Can't get more work - shutdown idle nodes. This will force them
# to run the last test now instead of waiting for more tests.
for node in idle_nodes:
node.shutdown()
return
assert steal_from is not None
steal_from.node.send_steal(steal_from.pending[-num_steal:])
self.steal_requested_from_node = steal_from.node
def remove_node(self, node: WorkerController) -> str | None:
"""Remove a node from the scheduler.
This should be called either when the node crashed or at
shutdown time. In the former case any pending items assigned
to the node will be re-scheduled. Called by the
``DSession.worker_workerfinished`` and
``DSession.worker_errordown`` hooks.
Return the item which was being executing while the node
crashed or None if the node has no more pending items.
"""
pending = self.node2pending.pop(node)
# If node was removed without completing its assigned tests - it crashed
if pending:
assert self.collection is not None
crashitem = self.collection[pending.pop(0)]
else:
crashitem = None
self.pending.extend(pending)
# Dead node won't respond to "steal" request
if self.steal_requested_from_node is node:
self.steal_requested_from_node = None
self.check_schedule()
return crashitem
def schedule(self) -> None:
"""Initiate distribution of the test collection.
Initiate scheduling of the items across the nodes. If this
gets called again later it behaves the same as calling
``.check_schedule()`` on all nodes so that newly added nodes
will start to be used.
This is called by the ``DSession.worker_collectionfinish`` hook
if ``.collection_is_completed`` is True.
"""
assert self.collection_is_completed
# Initial distribution already happened, reschedule on all nodes
if self.collection is not None:
self.check_schedule()
return
if not self._check_nodes_have_same_collection():
self.log("**Different tests collected, aborting run**")
return
# Collections are identical, create the index of pending items.
self.collection = next(iter(self.node2collection.values()))
self.pending[:] = range(len(self.collection))
if not self.collection:
return
self.check_schedule()
def _send_tests(self, node: WorkerController, num: int) -> None:
tests_per_node = self.pending[:num]
if tests_per_node:
del self.pending[:num]
self.node2pending[node].extend(tests_per_node)
node.send_runtest_some(tests_per_node)
def _check_nodes_have_same_collection(self) -> bool:
"""Return True if all nodes have collected the same items.
If collections differ, this method returns False while logging
the collection differences and posting collection errors to
pytest_collectreport hook.
"""
node_collection_items = list(self.node2collection.items())
first_node, col = node_collection_items[0]
same_collection = True
for node, collection in node_collection_items[1:]:
msg = report_collection_diff(
col, collection, first_node.gateway.id, node.gateway.id
)
if msg:
same_collection = False
self.log(msg)
if self.config is not None:
rep = pytest.CollectReport(
nodeid=node.gateway.id,
outcome="failed",
longrepr=msg,
result=[],
)
self.config.hook.pytest_collectreport(report=rep)
return same_collection

508
src/xdist/workermanage.py Normal file
View File

@@ -0,0 +1,508 @@
from __future__ import annotations
from collections.abc import Sequence
import enum
import fnmatch
import os
from pathlib import Path
import re
import sys
from typing import Any
from typing import Callable
from typing import Literal
from typing import Union
import uuid
import warnings
import execnet
import pytest
from xdist.plugin import _sys_path
import xdist.remote
from xdist.remote import Producer
from xdist.remote import WorkerInfo
def parse_tx_spec_config(config: pytest.Config) -> list[str]:
xspeclist = []
tx: list[str] = config.getvalue("tx")
for xspec in tx:
i = xspec.find("*")
try:
num = int(xspec[:i])
except ValueError:
xspeclist.append(xspec)
else:
xspeclist.extend([xspec[i + 1 :]] * num)
if not xspeclist:
raise pytest.UsageError(
"MISSING test execution (tx) nodes: please specify --tx"
)
return xspeclist
class NodeManager:
EXIT_TIMEOUT = 10
DEFAULT_IGNORES = [".*", "*.pyc", "*.pyo", "*~"]
def __init__(
self,
config: pytest.Config,
specs: Sequence[execnet.XSpec | str] | None = None,
defaultchdir: str = "pyexecnetcache",
) -> None:
self.config = config
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(execmodel="main_thread_only")
for proxy_spec in self._getpxspecs():
# Proxy gateways do not run workers, and are meant to be passed with the `via` attribute
# to additional gateways.
# They are useful for running multiple workers on remote machines.
if getattr(proxy_spec, "id", None) is None:
raise pytest.UsageError(
f"Proxy gateway {proxy_spec} must include an id"
)
self.group.makegateway(proxy_spec)
if specs is None:
specs = self._gettxspecs()
self.specs: list[execnet.XSpec] = []
for spec in specs:
if not isinstance(spec, execnet.XSpec):
spec = execnet.XSpec(spec)
if getattr(spec, "execmodel", None) != "main_thread_only":
spec = execnet.XSpec(f"execmodel=main_thread_only//{spec}")
if not spec.chdir and not spec.popen:
spec.chdir = defaultchdir
self.group.allocate_id(spec)
self.specs.append(spec)
self.roots = self._getrsyncdirs()
self.rsyncoptions = self._getrsyncoptions()
self._rsynced_specs: set[tuple[Any, Any]] = set()
def rsync_roots(self, gateway: execnet.Gateway) -> None:
"""Rsync the set of roots to the node's gateway cwd."""
if self.roots:
for root in self.roots:
self.rsync(gateway, root, **self.rsyncoptions)
def setup_nodes(
self,
putevent: Callable[[tuple[str, dict[str, Any]]], None],
) -> list[WorkerController]:
self.config.hook.pytest_xdist_setupnodes(config=self.config, specs=self.specs)
self.trace("setting up nodes")
return [self.setup_node(spec, putevent) for spec in self.specs]
def setup_node(
self,
spec: execnet.XSpec,
putevent: Callable[[tuple[str, dict[str, Any]]], None],
) -> WorkerController:
if getattr(spec, "execmodel", None) != "main_thread_only":
spec = execnet.XSpec(f"execmodel=main_thread_only//{spec}")
gw = self.group.makegateway(spec)
self.config.hook.pytest_xdist_newgateway(gateway=gw)
self.rsync_roots(gw)
node = WorkerController(self, gw, self.config, putevent)
# Keep the node alive.
gw.node = node # type: ignore[attr-defined]
node.setup()
self.trace("started node %r" % node)
return node
def teardown_nodes(self) -> None:
self.group.terminate(self.EXIT_TIMEOUT)
def _gettxspecs(self) -> list[execnet.XSpec]:
return [execnet.XSpec(x) for x in parse_tx_spec_config(self.config)]
def _getpxspecs(self) -> list[execnet.XSpec]:
return [execnet.XSpec(x) for x in self.config.getoption("px")]
def _getrsyncdirs(self) -> list[Path]:
for spec in self.specs:
if not spec.popen or spec.chdir:
break
else:
return []
import _pytest
import pytest
def get_dir(p: str) -> str:
"""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
candidates = [pytestpath, pytestdir]
candidates += config.option.rsyncdir
rsyncroots = config.getini("rsyncdirs")
if rsyncroots:
candidates.extend(rsyncroots)
roots = []
for root in candidates:
root_path = Path(root).resolve()
if not root_path.exists():
raise pytest.UsageError(f"rsyncdir doesn't exist: {root!r}")
if root_path not in roots:
roots.append(root_path)
return roots
def _getrsyncoptions(self) -> dict[str, Any]:
"""Get options to be passed for rsync."""
ignores = list(self.DEFAULT_IGNORES)
ignores += [str(path) for path in self.config.option.rsyncignore]
ignores += [str(path) for path in self.config.getini("rsyncignore")]
return {
"ignores": ignores,
"verbose": getattr(self.config.option, "verbose", 0),
}
def rsync(
self,
gateway: execnet.Gateway,
source: str | os.PathLike[str],
notify: (
Callable[[str, execnet.XSpec, str | os.PathLike[str]], Any] | None
) = None,
verbose: int = False,
ignores: Sequence[str] | None = None,
) -> None:
"""Perform rsync to remote hosts for node."""
# XXX This changes the calling behaviour of
# pytest_xdist_rsyncstart and pytest_xdist_rsyncfinish to
# be called once per rsync target.
rsync = HostRSync(source, verbose=verbose > 0, ignores=ignores)
spec = gateway.spec
if spec.popen and not spec.chdir:
# XXX This assumes that sources are python-packages
# and that adding the basedir does not hurt.
gateway.remote_exec(
"""
import sys ; sys.path.insert(0, %r)
"""
% os.path.dirname(str(source))
).waitclose()
return
if (spec, source) in self._rsynced_specs:
return
def finished() -> None:
if notify:
notify("rsyncrootready", spec, source)
rsync.add_target_host(gateway, finished=finished)
self._rsynced_specs.add((spec, source))
self.config.hook.pytest_xdist_rsyncstart(source=source, gateways=[gateway])
rsync.send()
self.config.hook.pytest_xdist_rsyncfinish(source=source, gateways=[gateway])
class HostRSync(execnet.RSync):
"""RSyncer that filters out common files."""
PathLike = Union[str, "os.PathLike[str]"]
def __init__(
self,
sourcedir: PathLike,
*,
ignores: Sequence[PathLike] | None = None,
verbose: bool = True,
) -> None:
if ignores is None:
ignores = []
self._ignores = [re.compile(fnmatch.translate(os.fspath(x))) for x in ignores]
super().__init__(sourcedir=Path(sourcedir), verbose=verbose)
def filter(self, path: PathLike) -> bool:
path = Path(path)
for cre in self._ignores:
if cre.match(path.name) or cre.match(str(path)):
return False
else:
return True
def add_target_host(
self,
gateway: execnet.Gateway,
finished: Callable[[], None] | None = None,
) -> None:
remotepath = os.path.basename(self._sourcedir)
super().add_target(gateway, remotepath, finishedcallback=finished, delete=True)
def _report_send_file(
self,
gateway: execnet.Gateway, # type: ignore[override]
modified_rel_path: str,
) -> None:
if self._verbose > 0:
path = os.path.basename(self._sourcedir) + "/" + modified_rel_path
remotepath = gateway.spec.chdir
print(f"{gateway.spec}:{remotepath} <= {path}")
def make_reltoroot(roots: Sequence[Path], args: list[str]) -> list[str]:
# XXX introduce/use public API for splitting pytest args
splitcode = "::"
result = []
for arg in args:
parts = arg.split(splitcode)
fspath = Path(parts[0])
try:
exists = fspath.exists()
except OSError:
exists = False
if not exists:
result.append(arg)
continue
for root in roots:
x: Path | None
try:
x = fspath.relative_to(root)
except ValueError:
x = None
if x or fspath == root:
parts[0] = root.name + "/" + str(x)
break
else:
raise ValueError(f"arg {arg} not relative to an rsync root")
result.append(splitcode.join(parts))
return result
class Marker(enum.Enum):
END = -1
class WorkerController:
# Set when the worker is ready.
workerinfo: WorkerInfo
class RemoteHook:
@pytest.hookimpl(trylast=True)
def pytest_xdist_getremotemodule(self) -> Any:
return xdist.remote
def __init__(
self,
nodemanager: NodeManager,
gateway: execnet.Gateway,
config: pytest.Config,
putevent: Callable[[tuple[str, dict[str, Any]]], None],
) -> None:
config.pluginmanager.register(self.RemoteHook())
self.nodemanager = nodemanager
self.putevent = putevent
self.gateway = gateway
self.config = config
self.workerinput = {
"workerid": gateway.id,
"workercount": len(nodemanager.specs),
"testrunuid": nodemanager.testrunuid,
"mainargv": sys.argv,
}
self._down = False
self._shutdown_sent = False
self.log = Producer(f"workerctl-{gateway.id}", enabled=config.option.debug)
def __repr__(self) -> str:
return f"<{self.__class__.__name__} {self.gateway.id}>"
@property
def shutting_down(self) -> bool:
return self._down or self._shutdown_sent
def setup(self) -> None:
self.log("setting up worker session")
# Cache rinfo for backward compatibility, since pytest-cov
# accesses rinfo while the main thread is busy executing our
# remote_exec call, which triggers a deadlock error for the
# main_thread_only execmodel if the rinfo has not been cached.
self.gateway._rinfo()
spec = self.gateway.spec
args = [str(x) for x in self.config.invocation_params.args or ()]
option_dict = {}
if not spec.popen or spec.chdir:
args = make_reltoroot(self.nodemanager.roots, args)
if spec.popen:
name = "popen-%s" % self.gateway.id
if hasattr(self.config, "_tmp_path_factory"):
basetemp = self.config._tmp_path_factory.getbasetemp()
option_dict["basetemp"] = str(basetemp / name)
self.config.hook.pytest_configure_node(node=self)
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))
# putevent is only None in a test.
if self.putevent: # type: ignore[truthy-function]
self.channel.setcallback(self.process_from_remote, endmarker=Marker.END)
def ensure_teardown(self) -> None:
if hasattr(self, "channel"):
if not self.channel.isclosed():
self.log("closing", self.channel)
self.channel.close()
# del self.channel
if hasattr(self, "gateway"):
self.log("exiting", self.gateway)
self.gateway.exit()
# del self.gateway
def send_runtest_some(self, indices: Sequence[int]) -> None:
self.sendcommand("runtests", indices=indices)
def send_runtest_all(self) -> None:
self.sendcommand("runtests_all")
def send_steal(self, indices: Sequence[int]) -> None:
self.sendcommand("steal", indices=indices)
def shutdown(self) -> None:
if not self._down:
try:
self.sendcommand("shutdown")
except OSError:
pass
self._shutdown_sent = True
def sendcommand(self, name: str, **kwargs: object) -> None:
"""Send a named parametrized command to the other side."""
self.log(f"sending command {name}(**{kwargs})")
self.channel.send((name, kwargs))
def notify_inproc(self, eventname: str, **kwargs: object) -> None:
self.log(f"queuing {eventname}(**{kwargs})")
self.putevent((eventname, kwargs))
def process_from_remote(
self, eventcall: tuple[str, dict[str, Any]] | Literal[Marker.END]
) -> None:
"""This gets called for each object we receive from
the other side and if the channel closes.
Note that channel callbacks run in the receiver
thread of execnet gateways - we need to
avoid raising exceptions or doing heavy work.
"""
try:
if eventcall is Marker.END:
err: object | None = self.channel._getremoteerror() # type: ignore[no-untyped-call]
if not self._down:
if not err or isinstance(err, EOFError):
err = "Not properly terminated" # lost connection?
self.notify_inproc("errordown", node=self, error=err)
self._down = True
return
eventname, kwargs = eventcall
if eventname in ("collectionstart",):
self.log(f"ignoring {eventname}({kwargs})")
elif eventname == "workerready":
self.notify_inproc(eventname, node=self, **kwargs)
elif eventname == "internal_error":
self.notify_inproc(eventname, node=self, **kwargs)
elif eventname == "workerfinished":
self._down = True
self.workeroutput = kwargs["workeroutput"]
self.notify_inproc("workerfinished", node=self)
elif eventname in ("logstart", "logfinish"):
self.notify_inproc(eventname, node=self, **kwargs)
elif eventname in ("testreport", "collectreport", "teardownreport"):
item_index = kwargs.pop("item_index", None)
rep = self.config.hook.pytest_report_from_serializable(
config=self.config, data=kwargs["data"]
)
if item_index is not None:
rep.item_index = item_index
self.notify_inproc(eventname, node=self, rep=rep)
elif eventname == "collectionfinish":
self.notify_inproc(eventname, node=self, ids=kwargs["ids"])
elif eventname == "runtest_protocol_complete":
self.notify_inproc(eventname, node=self, **kwargs)
elif eventname == "unscheduled":
self.notify_inproc(eventname, node=self, **kwargs)
elif eventname == "logwarning":
self.notify_inproc(
eventname,
message=kwargs["message"],
code=kwargs["code"],
nodeid=kwargs["nodeid"],
fslocation=kwargs["nodeid"],
)
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:
raise ValueError(f"unknown event: {eventname}")
except KeyboardInterrupt:
# should not land in receiver-thread
raise
except BaseException:
excinfo = pytest.ExceptionInfo.from_current()
print("!" * 20, excinfo)
self.config.notify_exception(excinfo)
self.shutdown()
self.notify_inproc("errordown", node=self, error=excinfo)
def unserialize_warning_message(data: dict[str, Any]) -> warnings.WarningMessage:
import importlib
if data["message_module"]:
mod = importlib.import_module(data["message_module"])
cls = getattr(mod, data["message_class_name"])
message = None
if data["message_args"] is not None:
try:
message = cls(*data["message_args"])
except TypeError:
pass
if message is None:
# could not recreate the original warning instance;
# create a generic Warning instance with the original
# message at least
message_text = "{mod}.{cls}: {msg}".format(
mod=data["message_module"],
cls=data["message_class_name"],
msg=data["message_str"],
)
message = Warning(message_text)
else:
message = data["message_str"]
if data["category_module"]:
mod = importlib.import_module(data["category_module"])
category = getattr(mod, data["category_class_name"])
else:
category = None
kwargs = {"message": message, "category": category}
# access private _WARNING_DETAILS because the attributes vary between Python versions
for attr_name in warnings.WarningMessage._WARNING_DETAILS: # type: ignore[attr-defined]
if attr_name in ("message", "category"):
continue
kwargs[attr_name] = data[attr_name]
return warnings.WarningMessage(**kwargs)

File diff suppressed because it is too large Load Diff

View File

@@ -1,73 +1,68 @@
import py
import pytest
from __future__ import annotations
from collections.abc import Generator
import shutil
from typing import Callable
import execnet
@pytest.fixture(scope="session", autouse=True)
def _ensure_imports():
# we import some modules because pytest-2.8's testdir fixture
# will unload all modules after each test and this cause
# (unknown) problems with execnet.Group()
execnet.Group
execnet.makegateway
import pytest
pytest_plugins = "pytester"
# rsyncdirs = ['.', '../xdist', py.path.local(execnet.__file__).dirpath()]
@pytest.fixture(autouse=True)
def _divert_atexit(request, monkeypatch):
def _divert_atexit(monkeypatch: pytest.MonkeyPatch) -> Generator[None]:
import atexit
l = []
def finish():
while l:
l.pop()()
finalizers = []
monkeypatch.setattr(atexit, "register", l.append)
request.addfinalizer(finish)
def fake_register(
func: Callable[..., object], *args: object, **kwargs: object
) -> None:
finalizers.append((func, args, kwargs))
monkeypatch.setattr(atexit, "register", fake_register)
yield
while finalizers:
func, args, kwargs = finalizers.pop()
func(*args, **kwargs)
def pytest_addoption(parser):
parser.addoption('--gx',
action="append",
dest="gspecs",
help=("add a global test environment, XSpec-syntax. "))
def pytest_addoption(parser: pytest.Parser) -> None:
parser.addoption(
"--gx",
action="append",
dest="gspecs",
help="add a global test environment, XSpec-syntax. ",
)
@pytest.fixture
def specssh(request):
def specssh(request: pytest.FixtureRequest) -> str:
return getspecssh(request.config)
@pytest.fixture
def testdir(testdir):
# pytest before 2.8 did not have a runpytest_subprocess
if not hasattr(testdir, "runpytest_subprocess"):
testdir.runpytest_subprocess = testdir.runpytest
return testdir
# configuration information for tests
def getgspecs(config):
def getgspecs(config: pytest.Config) -> list[execnet.XSpec]:
return [execnet.XSpec(spec) for spec in config.getvalueorskip("gspecs")]
def getspecssh(config):
def getspecssh(config: pytest.Config) -> str:
xspecs = getgspecs(config)
for spec in xspecs:
if spec.ssh:
if not py.path.local.sysfind("ssh"):
py.test.skip("command not found: ssh")
if not shutil.which("ssh"):
pytest.skip("command not found: ssh")
return str(spec)
py.test.skip("need '--gx ssh=...'")
pytest.skip("need '--gx ssh=...'")
def getsocketspec(config):
def getsocketspec(config: pytest.Config) -> execnet.XSpec:
xspecs = getgspecs(config)
for spec in xspecs:
if spec.socket:
return spec
py.test.skip("need '--gx socket=...'")
pytest.skip("need '--gx socket=...'")

View File

@@ -1,58 +0,0 @@
import pytest
import os
needsfork = pytest.mark.skipif(not hasattr(os, "fork"),
reason="os.fork required")
@needsfork
def test_functional_boxed(testdir):
p1 = testdir.makepyfile("""
import os
def test_function():
os.kill(os.getpid(), 15)
""")
result = testdir.runpytest(p1, "--boxed")
result.stdout.fnmatch_lines([
"*CRASHED*",
"*1 failed*"
])
@needsfork
@pytest.mark.parametrize("capmode", [
"no",
pytest.mark.xfail("sys", reason="capture cleanup needed"),
pytest.mark.xfail("fd", reason="capture cleanup needed")])
def test_functional_boxed_capturing(testdir, capmode):
p1 = testdir.makepyfile("""
import os
import sys
def test_function():
sys.stdout.write("hello\\n")
sys.stderr.write("world\\n")
os.kill(os.getpid(), 15)
""")
result = testdir.runpytest(p1, "--boxed", "--capture=%s" % capmode)
result.stdout.fnmatch_lines("""
*CRASHED*
*stdout*
hello
*stderr*
world
*1 failed*
""")
class TestOptionEffects:
def test_boxed_option_default(self, testdir):
tmpdir = testdir.tmpdir.ensure("subdir", dir=1)
config = testdir.parseconfig()
assert not config.option.boxed
pytest.importorskip("execnet")
config = testdir.parseconfig('-d', tmpdir)
assert not config.option.boxed
def test_is_not_boxed_by_default(self, testdir):
config = testdir.parseconfig(testdir.tmpdir)
assert not config.option.boxed

View File

@@ -1,87 +1,96 @@
from xdist.dsession import (
DSession, LoadScheduling, EachScheduling, report_collection_diff,
)
import py
import pytest
from __future__ import annotations
from collections.abc import Sequence
from typing import Any
from typing import cast
from typing import TYPE_CHECKING
import execnet
import pytest
XSpec = execnet.XSpec
from xdist.dsession import DSession
from xdist.dsession import get_default_max_worker_restart
from xdist.dsession import get_workers_status_line
from xdist.dsession import WorkerStatus
from xdist.report import report_collection_diff
from xdist.scheduler import EachScheduling
from xdist.scheduler import LoadScheduling
from xdist.scheduler import WorkStealingScheduling
from xdist.workermanage import WorkerController
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
if TYPE_CHECKING:
BaseOfMockGateway = execnet.Gateway
BaseOfMockNode = WorkerController
else:
BaseOfMockGateway = object
BaseOfMockNode = object
class MockGateway:
_count = 0
def __init__(self):
class MockGateway(BaseOfMockGateway):
def __init__(self) -> None:
self._count = 0
self.id = str(self._count)
self._count += 1
class MockNode:
def __init__(self):
self.sent = []
class MockNode(BaseOfMockNode):
def __init__(self) -> None:
self.sent: list[int | str] = []
self.stolen: list[int] = []
self.gateway = MockGateway()
self._shutdown = False
def send_runtest_some(self, indices):
def send_runtest_some(self, indices: Sequence[int]) -> None:
self.sent.extend(indices)
def send_runtest_all(self):
def send_runtest_all(self) -> None:
self.sent.append("ALL")
def shutdown(self):
def send_steal(self, indices: Sequence[int]) -> None:
self.stolen.extend(indices)
def shutdown(self) -> None:
self._shutdown = True
@property
def shutting_down(self):
def shutting_down(self) -> bool:
return self._shutdown
def dumpqueue(queue):
while queue.qsize():
print(queue.get())
class TestEachScheduling:
def test_schedule_load_simple(self, testdir):
node1 = MockNode()
node2 = MockNode()
config = testdir.parseconfig("--tx=2*popen")
def test_schedule_load_simple(self, pytester: pytest.Pytester) -> None:
config = pytester.parseconfig("--tx=2*popen")
sched = EachScheduling(config)
node1, node2 = MockNode(), MockNode()
sched.add_node(node1)
sched.add_node(node2)
collection = ["a.py::test_1", ]
collection = ["a.py::test_1"]
assert not sched.collection_is_completed
sched.add_node_collection(node1, collection)
assert not sched.collection_is_completed
sched.add_node_collection(node2, collection)
assert sched.collection_is_completed
assert bool(sched.collection_is_completed)
assert sched.node2collection[node1] == collection
assert sched.node2collection[node2] == collection
sched.schedule()
assert sched.tests_finished
assert node1.sent == ['ALL']
assert node2.sent == ['ALL']
assert node1.sent == ["ALL"]
assert node2.sent == ["ALL"]
sched.mark_test_complete(node1, 0)
assert sched.tests_finished
sched.mark_test_complete(node2, 0)
assert sched.tests_finished
def test_schedule_remove_node(self, testdir):
node1 = MockNode()
config = testdir.parseconfig("--tx=popen")
def test_schedule_remove_node(self, pytester: pytest.Pytester) -> None:
config = pytester.parseconfig("--tx=popen")
sched = EachScheduling(config)
node1 = MockNode()
sched.add_node(node1)
collection = ["a.py::test_1", ]
collection = ["a.py::test_1"]
assert not sched.collection_is_completed
sched.add_node_collection(node1, collection)
assert sched.collection_is_completed
assert bool(sched.collection_is_completed)
assert sched.node2collection[node1] == collection
sched.schedule()
assert sched.tests_finished
@@ -92,18 +101,18 @@ class TestEachScheduling:
class TestLoadScheduling:
def test_schedule_load_simple(self, testdir):
config = testdir.parseconfig("--tx=2*popen")
def test_schedule_load_simple(self, pytester: pytest.Pytester) -> None:
config = pytester.parseconfig("--tx=2*popen")
sched = LoadScheduling(config)
sched.add_node(MockNode())
sched.add_node(MockNode())
node1, node2 = sched.nodes
node1, node2 = MockNode(), MockNode()
sched.add_node(node1)
sched.add_node(node2)
collection = ["a.py::test_1", "a.py::test_2"]
assert not sched.collection_is_completed
sched.add_node_collection(node1, collection)
assert not sched.collection_is_completed
sched.add_node_collection(node2, collection)
assert sched.collection_is_completed
assert bool(sched.collection_is_completed)
assert sched.node2collection[node1] == collection
assert sched.node2collection[node2] == collection
sched.schedule()
@@ -113,80 +122,134 @@ class TestLoadScheduling:
assert len(node2.sent) == 1
assert node1.sent == [0]
assert node2.sent == [1]
sched.mark_test_complete(node1, node1.sent[0])
sent10 = node1.sent[0]
assert isinstance(sent10, int)
sched.mark_test_complete(node1, sent10)
assert sched.tests_finished
def test_schedule_batch_size(self, testdir):
config = testdir.parseconfig("--tx=2*popen")
def test_schedule_batch_size(self, pytester: pytest.Pytester) -> None:
config = pytester.parseconfig("--tx=2*popen")
sched = LoadScheduling(config)
sched.add_node(MockNode())
sched.add_node(MockNode())
node1, node2 = sched.nodes
col = ["xyz"] * (6)
node1, node2 = MockNode(), MockNode()
sched.add_node(node1)
sched.add_node(node2)
col = ["xyz"] * 6
sched.add_node_collection(node1, col)
sched.add_node_collection(node2, col)
sched.schedule()
# assert not sched.tests_finished
sent1 = node1.sent
sent2 = node2.sent
assert sent1 == [0, 2]
assert sent2 == [1, 3]
assert sent1 == [0, 1]
assert sent2 == [2, 3]
assert sched.pending == [4, 5]
assert sched.node2pending[node1] == sent1
assert sched.node2pending[node2] == sent2
assert len(sched.pending) == 2
sched.mark_test_complete(node1, 0)
assert node1.sent == [0, 2, 4]
assert node1.sent == [0, 1, 4]
assert sched.pending == [5]
assert node2.sent == [1, 3]
sched.mark_test_complete(node1, 2)
assert node1.sent == [0, 2, 4, 5]
assert node2.sent == [2, 3]
sched.mark_test_complete(node1, 1)
assert node1.sent == [0, 1, 4, 5]
assert not sched.pending
def test_schedule_fewer_tests_than_nodes(self, testdir):
config = testdir.parseconfig("--tx=2*popen")
def test_schedule_maxchunk_none(self, pytester: pytest.Pytester) -> None:
config = pytester.parseconfig("--tx=2*popen")
sched = LoadScheduling(config)
sched.add_node(MockNode())
sched.add_node(MockNode())
sched.add_node(MockNode())
node1, node2, node3 = sched.nodes
node1, node2 = MockNode(), MockNode()
sched.add_node(node1)
sched.add_node(node2)
col = [f"test{i}" for i in range(16)]
sched.add_node_collection(node1, col)
sched.add_node_collection(node2, col)
sched.schedule()
assert node1.sent == [0, 1]
assert node2.sent == [2, 3]
assert sched.pending == list(range(4, 16))
assert sched.node2pending[node1] == node1.sent
assert sched.node2pending[node2] == node2.sent
sched.mark_test_complete(node1, 0)
assert node1.sent == [0, 1, 4, 5]
assert sched.pending == list(range(6, 16))
sched.mark_test_complete(node1, 1)
assert node1.sent == [0, 1, 4, 5]
assert sched.pending == list(range(6, 16))
for i in range(7, 16):
sched.mark_test_complete(node1, i - 3)
assert node1.sent == [0, 1, *range(4, i)]
assert node2.sent == [2, 3]
assert sched.pending == list(range(i, 16))
def test_schedule_maxchunk_1(self, pytester: pytest.Pytester) -> None:
config = pytester.parseconfig("--tx=2*popen", "--maxschedchunk=1")
sched = LoadScheduling(config)
node1, node2 = MockNode(), MockNode()
sched.add_node(node1)
sched.add_node(node2)
col = [f"test{i}" for i in range(16)]
sched.add_node_collection(node1, col)
sched.add_node_collection(node2, col)
sched.schedule()
assert node1.sent == [0, 1]
assert node2.sent == [2, 3]
assert sched.pending == list(range(4, 16))
assert sched.node2pending[node1] == node1.sent
assert sched.node2pending[node2] == node2.sent
for complete_index, first_pending in enumerate(range(5, 16)):
sent_index = node1.sent[complete_index]
assert isinstance(sent_index, int)
sched.mark_test_complete(node1, sent_index)
assert node1.sent == [0, 1, *range(4, first_pending)]
assert node2.sent == [2, 3]
assert sched.pending == list(range(first_pending, 16))
def test_schedule_fewer_tests_than_nodes(self, pytester: pytest.Pytester) -> None:
config = pytester.parseconfig("--tx=3*popen")
sched = LoadScheduling(config)
node1, node2, node3 = MockNode(), MockNode(), MockNode()
sched.add_node(node1)
sched.add_node(node2)
sched.add_node(node3)
col = ["xyz"] * 2
sched.add_node_collection(node1, col)
sched.add_node_collection(node2, col)
sched.add_node_collection(node3, col)
assert sched.collection_is_completed
sched.schedule()
# assert not sched.tests_finished
sent1 = node1.sent
sent2 = node2.sent
sent3 = node3.sent
assert sent1 == [0]
assert sent2 == [1]
assert sent3 == []
assert node1.sent == [0]
assert node2.sent == [1]
assert node3.sent == []
assert not sched.pending
def test_schedule_fewer_than_two_tests_per_node(self, testdir):
config = testdir.parseconfig("--tx=2*popen")
def test_schedule_fewer_than_two_tests_per_node(
self, pytester: pytest.Pytester
) -> None:
config = pytester.parseconfig("--tx=3*popen")
sched = LoadScheduling(config)
sched.add_node(MockNode())
sched.add_node(MockNode())
sched.add_node(MockNode())
node1, node2, node3 = sched.nodes
node1, node2, node3 = MockNode(), MockNode(), MockNode()
sched.add_node(node1)
sched.add_node(node2)
sched.add_node(node3)
col = ["xyz"] * 5
sched.add_node_collection(node1, col)
sched.add_node_collection(node2, col)
sched.add_node_collection(node3, col)
assert sched.collection_is_completed
sched.schedule()
# assert not sched.tests_finished
sent1 = node1.sent
sent2 = node2.sent
sent3 = node3.sent
assert sent1 == [0, 3]
assert sent2 == [1, 4]
assert sent3 == [2]
assert node1.sent == [0, 3]
assert node2.sent == [1, 4]
assert node3.sent == [2]
assert not sched.pending
def test_add_remove_node(self, testdir):
node = MockNode()
config = testdir.parseconfig("--tx=popen")
def test_add_remove_node(self, pytester: pytest.Pytester) -> None:
config = pytester.parseconfig("--tx=popen")
sched = LoadScheduling(config)
node = MockNode()
sched.add_node(node)
collection = ["test_file.py::test_func"]
sched.add_node_collection(node, collection)
@@ -196,29 +259,26 @@ class TestLoadScheduling:
crashitem = sched.remove_node(node)
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
different test ids are collected by slaves.
different test ids are collected by workers.
"""
class CollectHook(object):
"""
Dummy hook that stores collection reports.
"""
class CollectHook:
"""Dummy hook that stores collection reports."""
def __init__(self):
self.reports = []
def __init__(self) -> None:
self.reports: list[pytest.CollectReport] = []
def pytest_collectreport(self, report):
def pytest_collectreport(self, report: pytest.CollectReport) -> None:
self.reports.append(report)
collect_hook = CollectHook()
config = testdir.parseconfig("--tx=2*popen")
config = pytester.parseconfig("--tx=2*popen")
config.pluginmanager.register(collect_hook, "collect_hook")
node1 = MockNode()
node2 = MockNode()
sched = LoadScheduling(config)
node1, node2 = MockNode(), MockNode()
sched.add_node(node1)
sched.add_node(node2)
sched.add_node_collection(node1, ["a.py::test_1"])
@@ -226,14 +286,193 @@ class TestLoadScheduling:
sched.schedule()
assert len(collect_hook.reports) == 1
rep = collect_hook.reports[0]
assert 'Different tests were collected between' in rep.longrepr
assert isinstance(rep.longrepr, str)
assert "Different tests were collected between" in rep.longrepr
class TestWorkStealingScheduling:
def test_ideal_case(self, pytester: pytest.Pytester) -> None:
config = pytester.parseconfig("--tx=2*popen")
sched = WorkStealingScheduling(config)
node1, node2 = MockNode(), MockNode()
sched.add_node(node1)
sched.add_node(node2)
collection = [f"test_workstealing.py::test_{i}" for i in range(16)]
assert not sched.collection_is_completed
sched.add_node_collection(node1, collection)
assert not sched.collection_is_completed
sched.add_node_collection(node2, collection)
assert bool(sched.collection_is_completed)
assert sched.node2collection[node1] == collection
assert sched.node2collection[node2] == collection
sched.schedule()
assert not sched.pending
assert not sched.tests_finished
assert node1.sent == list(range(8))
assert node2.sent == list(range(8, 16))
for i in range(8):
sent1, sent2 = node1.sent[i], node2.sent[i]
assert isinstance(sent1, int) and isinstance(sent2, int)
sched.mark_test_complete(node1, sent1)
sched.mark_test_complete(node2, sent2)
assert bool(sched.tests_finished)
assert node1.stolen == []
assert node2.stolen == []
def test_stealing(self, pytester: pytest.Pytester) -> None:
config = pytester.parseconfig("--tx=2*popen")
sched = WorkStealingScheduling(config)
node1, node2 = MockNode(), MockNode()
sched.add_node(node1)
sched.add_node(node2)
collection = [f"test_workstealing.py::test_{i}" for i in range(16)]
sched.add_node_collection(node1, collection)
sched.add_node_collection(node2, collection)
assert sched.collection_is_completed
sched.schedule()
assert node1.sent == list(range(8))
assert node2.sent == list(range(8, 16))
for i in range(8):
sent = node1.sent[i]
assert isinstance(sent, int)
sched.mark_test_complete(node1, sent)
assert node2.stolen == list(range(12, 16))
sched.remove_pending_tests_from_node(node2, node2.stolen)
for i in range(4):
sent = node2.sent[i]
assert isinstance(sent, int)
sched.mark_test_complete(node2, sent)
assert node1.stolen == [14, 15]
sched.remove_pending_tests_from_node(node1, node1.stolen)
sched.mark_test_complete(node1, 12)
sched.mark_test_complete(node2, 14)
assert node2.stolen == list(range(12, 16))
assert node1.stolen == [14, 15]
assert sched.tests_finished
def test_steal_on_add_node(self, pytester: pytest.Pytester) -> None:
node = MockNode()
config = pytester.parseconfig("--tx=popen")
sched = WorkStealingScheduling(config)
sched.add_node(node)
collection = [f"test_workstealing.py::test_{i}" for i in range(5)]
sched.add_node_collection(node, collection)
assert sched.collection_is_completed
sched.schedule()
assert not sched.pending
sched.mark_test_complete(node, 0)
node2 = MockNode()
sched.add_node(node2)
sched.add_node_collection(node2, collection)
assert sched.collection_is_completed
sched.schedule()
assert node.stolen == [3, 4]
sched.remove_pending_tests_from_node(node, node.stolen)
sched.mark_test_complete(node, 1)
sched.mark_test_complete(node2, 3)
assert sched.tests_finished
assert node2.stolen == []
def test_schedule_fewer_tests_than_nodes(self, pytester: pytest.Pytester) -> None:
config = pytester.parseconfig("--tx=3*popen")
sched = WorkStealingScheduling(config)
node1, node2, node3 = MockNode(), MockNode(), MockNode()
sched.add_node(node1)
sched.add_node(node2)
sched.add_node(node3)
col = ["xyz"] * 2
sched.add_node_collection(node1, col)
sched.add_node_collection(node2, col)
sched.add_node_collection(node3, col)
sched.schedule()
assert node1.sent == []
assert node1.stolen == []
assert node2.sent == [0]
assert node2.stolen == []
assert node3.sent == [1]
assert node3.stolen == []
assert not sched.pending
assert sched.tests_finished
def test_schedule_fewer_than_two_tests_per_node(
self, pytester: pytest.Pytester
) -> None:
config = pytester.parseconfig("--tx=3*popen")
sched = WorkStealingScheduling(config)
node1, node2, node3 = MockNode(), MockNode(), MockNode()
sched.add_node(node1)
sched.add_node(node2)
sched.add_node(node3)
col = ["xyz"] * 5
sched.add_node_collection(node1, col)
sched.add_node_collection(node2, col)
sched.add_node_collection(node3, col)
sched.schedule()
assert node1.sent == [0]
assert node2.sent == [1, 2]
assert node3.sent == [3, 4]
assert not sched.pending
assert not sched.tests_finished
sent10 = node1.sent[0]
assert isinstance(sent10, int)
sent20 = node2.sent[0]
assert isinstance(sent20, int)
sent30 = node3.sent[0]
assert isinstance(sent30, int)
sent31 = node3.sent[1]
assert isinstance(sent31, int)
sched.mark_test_complete(node1, sent10)
sched.mark_test_complete(node2, sent20)
sched.mark_test_complete(node3, sent30)
sched.mark_test_complete(node3, sent31)
assert bool(sched.tests_finished)
assert node1.stolen == []
assert node2.stolen == []
assert node3.stolen == []
def test_add_remove_node(self, pytester: pytest.Pytester) -> None:
node = MockNode()
config = pytester.parseconfig("--tx=popen")
sched = WorkStealingScheduling(config)
sched.add_node(node)
collection = ["test_file.py::test_func"]
sched.add_node_collection(node, collection)
assert sched.collection_is_completed
sched.schedule()
assert not sched.pending
crashitem = sched.remove_node(node)
assert crashitem == collection[0]
def test_different_tests_collected(self, pytester: pytest.Pytester) -> None:
class CollectHook:
def __init__(self) -> None:
self.reports: list[pytest.CollectReport] = []
def pytest_collectreport(self, report: pytest.CollectReport) -> None:
self.reports.append(report)
collect_hook = CollectHook()
config = pytester.parseconfig("--tx=2*popen")
config.pluginmanager.register(collect_hook, "collect_hook")
sched = WorkStealingScheduling(config)
node1, node2 = MockNode(), MockNode()
sched.add_node(node1)
sched.add_node(node2)
sched.add_node_collection(node1, ["a.py::test_1"])
sched.add_node_collection(node2, ["a.py::test_2"])
sched.schedule()
assert len(collect_hook.reports) == 1
rep = collect_hook.reports[0]
assert isinstance(rep.longrepr, str)
assert "Different tests were collected between" in rep.longrepr
class TestDistReporter:
@py.test.mark.xfail
def test_rsync_printing(self, testdir, linecomp):
config = testdir.parseconfig()
from _pytest.pytest_terminal import TerminalReporter
@pytest.mark.xfail
def test_rsync_printing(self, pytester: pytest.Pytester, linecomp: Any) -> None:
config = pytester.parseconfig()
from _pytest.terminal import TerminalReporter
rep = TerminalReporter(config, file=linecomp.stringio)
config.pluginmanager.register(rep, "terminalreporter")
dsession = DSession(config)
@@ -245,6 +484,7 @@ class TestDistReporter:
class gw2:
id = "X2"
spec = execnet.XSpec("popen")
# class rinfo:
# version_info = (2, 5, 1, 'final', 0)
# executable = "hello"
@@ -255,48 +495,140 @@ class TestDistReporter:
# linecomp.assert_contains_lines([
# "*X1*popen*xyz*2.5*"
# ])
dsession.pytest_xdist_rsyncstart(source="hello", gateways=[gw1, gw2])
linecomp.assert_contains_lines(["[X1,X2] rsyncing: hello", ])
dsession.pytest_xdist_rsyncstart(source="hello", gateways=[gw1, gw2]) # type: ignore[attr-defined]
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."""
from_collection = to_collection = ['aaa', 'bbb', 'ccc']
assert report_collection_diff(from_collection, to_collection, 1, 2) is None
from_collection = to_collection = ["aaa", "bbb", "ccc"]
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 MockConfig:
class option:
maxworkerrestart: str | None = None
numprocesses: int = 0
config = cast(pytest.Config, MockConfig)
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."""
from_collection = ['aaa', 'bbb', 'ccc', 'YYY']
to_collection = ['aZa', 'bbb', 'XXX', 'ccc']
from_collection = ["aaa", "bbb", "ccc", "YYY"]
to_collection = ["aZa", "bbb", "XXX", "ccc"]
error_message = (
'Different tests were collected between 1 and 2. The difference is:\n'
'--- 1\n'
'\n'
'+++ 2\n'
'\n'
'@@ -1,4 +1,4 @@\n'
'\n'
'-aaa\n'
'+aZa\n'
' bbb\n'
'+XXX\n'
' ccc\n'
'-YYY')
"Different tests were collected between 1 and 2. The difference is:\n"
"--- 1\n"
"\n"
"+++ 2\n"
"\n"
"@@ -1,4 +1,4 @@\n"
"\n"
"-aaa\n"
"+aZa\n"
" bbb\n"
"+XXX\n"
" ccc\n"
"-YYY\n"
"To see why this happens see 'Known limitations' in documentation "
"for pytest-xdist"
)
msg = report_collection_diff(from_collection, to_collection, '1', '2')
msg = report_collection_diff(from_collection, to_collection, "1", "2")
assert msg == error_message
@pytest.mark.xfail(reason="duplicate test ids not supported yet")
def test_pytest_issue419(testdir):
testdir.makepyfile("""
def test_pytest_issue419(pytester: pytest.Pytester) -> None:
pytester.makepyfile(
"""
import pytest
@pytest.mark.parametrize('birth_year', [1988, 1988, ])
def test_2011_table(birth_year):
pass
""")
reprec = testdir.inline_run("-n1")
"""
)
reprec = pytester.inline_run("-n1")
reprec.assertoutcome(passed=2)
assert 0
Created = WorkerStatus.Created
Initialized = WorkerStatus.Initialized
ReadyForCollection = WorkerStatus.ReadyForCollection
CollectionDone = WorkerStatus.CollectionDone
@pytest.mark.parametrize(
"status_and_items, expected",
[
(
[],
"",
),
(
[(Created, 0)],
"created: 1/1 worker",
),
(
[(Created, 0), (Created, 0)],
"created: 2/2 workers",
),
(
[(Initialized, 0), (Created, 0)],
"initialized: 1/2 workers",
),
(
[(Initialized, 0), (Initialized, 0)],
"initialized: 2/2 workers",
),
(
[(ReadyForCollection, 0), (Created, 0)],
"ready: 1/2 workers",
),
(
[(ReadyForCollection, 0), (ReadyForCollection, 0)],
"ready: 2/2 workers",
),
(
[(CollectionDone, 12), (Created, 0)],
"collecting: 1/2 workers",
),
(
[(CollectionDone, 12), (CollectionDone, 12)],
"2 workers [12 items]",
),
(
[(CollectionDone, 1), (CollectionDone, 1)],
"2 workers [1 item]",
),
(
[(CollectionDone, 1)],
"1 worker [1 item]",
),
# Different number of tests collected will raise an error and should not happen in practice,
# but we test for it anyway.
(
[(CollectionDone, 1), (CollectionDone, 12)],
"2 workers [1 item]",
),
],
)
def test_get_workers_status_line(
status_and_items: Sequence[tuple[WorkerStatus, int]], expected: str
) -> None:
assert get_workers_status_line(status_and_items) == expected

View File

@@ -1,283 +1,402 @@
import py
from __future__ import annotations
import pathlib
from pathlib import Path
import shutil
import tempfile
import textwrap
import unittest.mock
import pytest
from xdist.looponfail import RemoteControl
from xdist.looponfail import StatRecorder
class TestStatRecorder:
def test_filechange(self, tmpdir):
tmp = tmpdir
hello = tmp.ensure("hello.py")
def test_filechange(self, tmp_path: Path) -> None:
tmp = tmp_path
hello = tmp / "hello.py"
hello.touch()
sd = StatRecorder([tmp])
changed = sd.check()
assert not changed
hello.write("world")
hello.write_text("world")
changed = sd.check()
assert changed
(hello + "c").write("hello")
hello.with_suffix(".pyc").write_text("hello")
changed = sd.check()
assert not changed
p = tmp.ensure("new.py")
p = tmp / "new.py"
p.touch()
changed = sd.check()
assert changed
p.remove()
p.unlink()
changed = sd.check()
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()
assert changed
tmp.join("a", "c.txt").ensure()
tmp.joinpath("a", "c.txt").touch()
changed = sd.check()
assert changed
changed = sd.check()
assert not changed
tmp.join("a").remove()
shutil.rmtree(str(tmp.joinpath("a")))
changed = sd.check()
assert changed
def test_dirchange(self, tmpdir):
tmp = tmpdir
tmp.ensure("dir", "hello.py")
def test_dirchange(self, tmp_path: Path) -> None:
tmp = tmp_path
tmp.joinpath("dir").mkdir()
tmp.joinpath("dir", "hello.py").touch()
sd = StatRecorder([tmp])
assert not sd.fil(tmp.join("dir"))
assert not sd.fil(tmp / "dir")
def test_filechange_deletion_race(self, tmpdir, monkeypatch):
tmp = tmpdir
def test_filechange_deletion_race(self, tmp_path: Path) -> None:
tmp = tmp_path
sd = StatRecorder([tmp])
changed = sd.check()
assert not changed
p = tmp.ensure("new.py")
p = tmp.joinpath("new.py")
p.touch()
changed = sd.check()
assert changed
p.remove()
p.unlink()
# make check()'s visit() call return our just removed
# path as if we were in a race condition
monkeypatch.setattr(tmp, 'visit', lambda *args: [p])
changed = sd.check()
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()
assert changed
def test_pycremoval(self, tmpdir):
tmp = tmpdir
hello = tmp.ensure("hello.py")
def test_pycremoval(self, tmp_path: Path) -> None:
tmp = tmp_path
hello = tmp / "hello.py"
hello.touch()
sd = StatRecorder([tmp])
changed = sd.check()
assert not changed
pycfile = hello + "c"
pycfile.ensure()
hello.write("world")
pycfile = hello.with_suffix(".pyc")
pycfile.touch()
hello.write_text("world")
changed = sd.check()
assert changed
assert not pycfile.check()
assert not pycfile.exists()
def test_waitonchange(self, tmpdir, monkeypatch):
tmp = tmpdir
def test_waitonchange(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
tmp = tmp_path
sd = StatRecorder([tmp])
l = [True, False]
monkeypatch.setattr(StatRecorder, 'check', lambda self: l.pop())
ret_values = [True, False]
monkeypatch.setattr(StatRecorder, "check", lambda self: ret_values.pop())
sd.waitonchange(checkinterval=0.2)
assert not l
assert not ret_values
class TestRemoteControl:
def test_nofailures(self, testdir):
item = testdir.getitem("def test_func(): pass\n")
def test_nofailures(self, pytester: pytest.Pytester) -> None:
item = pytester.getitem("def test_func(): pass\n")
control = RemoteControl(item.config)
control.setup()
topdir, failures = control.runsession()[:2]
assert not failures
def test_failures_somewhere(self, testdir):
item = testdir.getitem("def test_func():\n assert 0\n")
def test_failures_somewhere(self, pytester: pytest.Pytester) -> None:
item = pytester.getitem("def test_func():\n assert 0\n")
control = RemoteControl(item.config)
control.setup()
failures = control.runsession()
failures = control.runsession()[0]
assert failures
control.setup()
item.fspath.write("def test_func():\n assert 1\n")
removepyc(item.fspath)
item.path.write_text("def test_func():\n assert 1\n")
removepyc(item.path)
topdir, failures = control.runsession()[:2]
assert not failures
def test_failure_change(self, testdir):
modcol = testdir.getitem("""
def test_func():
assert 0
""")
def test_failure_change(self, pytester: pytest.Pytester) -> None:
modcol = pytester.getitem(
textwrap.dedent(
"""
def test_func():
assert 0
"""
)
)
control = RemoteControl(modcol.config)
control.loop_once()
assert control.failures
modcol.fspath.write(py.code.Source("""
def test_func():
assert 1
def test_new():
assert 0
"""))
removepyc(modcol.fspath)
modcol_path = modcol.path
modcol_path.write_text(
textwrap.dedent(
"""
def test_func():
assert 1
def test_new():
assert 0
"""
)
)
removepyc(modcol_path)
control.loop_once()
assert not control.failures
control.loop_once()
assert control.failures
assert str(control.failures).find("test_new") != -1
def test_failure_subdir_no_init(self, testdir):
modcol = testdir.getitem("""
def test_func():
assert 0
""")
parent = modcol.fspath.dirpath().dirpath()
parent.chdir()
modcol.config.args = [py.path.local(x).relto(parent)
for x in modcol.config.args]
def test_failure_subdir_no_init(
self, pytester: pytest.Pytester, monkeypatch: pytest.MonkeyPatch
) -> None:
modcol = pytester.getitem(
textwrap.dedent(
"""
def test_func():
assert 0
"""
)
)
parent = modcol.path.parent.parent
monkeypatch.chdir(parent)
modcol.config.args = [
str(Path(x).relative_to(parent)) for x in modcol.config.args
]
control = RemoteControl(modcol.config)
control.loop_once()
assert control.failures
control.loop_once()
assert control.failures
def test_ignore_sys_path_hook_entry(
self, pytester: pytest.Pytester, monkeypatch: pytest.MonkeyPatch
) -> None:
# Modifying sys.path as seen by the worker process is a bit tricky,
# because any changes made in the current process do not carry over.
# However, we can leverage the `sitecustomize` behavior to run arbitrary
# code when the subprocess interpreter is starting up. We just need to
# install our module in the search path, which we can accomplish by
# adding a temporary directory to PYTHONPATH.
tmpdir = tempfile.TemporaryDirectory()
with open(pathlib.Path(tmpdir.name) / "sitecustomize.py", "w") as custom:
print(
textwrap.dedent(
"""
import sys
sys.path.append('dummy.__path_hook__')
"""
),
file=custom,
)
monkeypatch.setenv("PYTHONPATH", tmpdir.name, prepend=":")
item = pytester.getitem(
textwrap.dedent(
"""
def test_func():
import sys
assert "dummy.__path_hook__" in sys.path
"""
)
)
control = RemoteControl(item.config)
control.setup()
topdir, failures = control.runsession()[:2]
assert not failures
class TestLooponFailing:
def test_looponfail_from_fail_to_ok(self, testdir):
modcol = testdir.getmodulecol("""
def test_one():
x = 0
assert x == 1
def test_two():
assert 1
""")
def test_looponfail_from_fail_to_ok(self, pytester: pytest.Pytester) -> None:
modcol = pytester.getmodulecol(
textwrap.dedent(
"""
def test_one():
x = 0
assert x == 1
def test_two():
assert 1
"""
)
)
remotecontrol = RemoteControl(modcol.config)
remotecontrol.loop_once()
assert len(remotecontrol.failures) == 1
modcol.fspath.write(py.code.Source("""
def test_one():
assert 1
def test_two():
assert 1
"""))
removepyc(modcol.fspath)
modcol.path.write_text(
textwrap.dedent(
"""
def test_one():
assert 1
def test_two():
assert 1
"""
)
)
removepyc(modcol.path)
remotecontrol.loop_once()
assert not remotecontrol.failures
def test_looponfail_from_one_to_two_tests(self, testdir):
modcol = testdir.getmodulecol("""
def test_one():
assert 0
""")
def test_looponfail_from_one_to_two_tests(self, pytester: pytest.Pytester) -> None:
modcol = pytester.getmodulecol(
textwrap.dedent(
"""
def test_one():
assert 0
"""
)
)
remotecontrol = RemoteControl(modcol.config)
remotecontrol.loop_once()
assert len(remotecontrol.failures) == 1
assert 'test_one' in remotecontrol.failures[0]
assert "test_one" in remotecontrol.failures[0]
modcol.fspath.write(py.code.Source("""
def test_one():
assert 1 # passes now
def test_two():
assert 0 # new and fails
"""))
removepyc(modcol.fspath)
modcol.path.write_text(
textwrap.dedent(
"""
def test_one():
assert 1 # passes now
def test_two():
assert 0 # new and fails
"""
)
)
removepyc(modcol.path)
remotecontrol.loop_once()
assert len(remotecontrol.failures) == 0
remotecontrol.loop_once()
assert len(remotecontrol.failures) == 1
assert 'test_one' not in remotecontrol.failures[0]
assert 'test_two' in remotecontrol.failures[0]
assert "test_one" not in remotecontrol.failures[0]
assert "test_two" in remotecontrol.failures[0]
def test_looponfail_removed_test(self, testdir):
modcol = testdir.getmodulecol("""
def test_one():
assert 0
def test_two():
assert 0
""")
@pytest.mark.xfail(reason="broken by pytest 3.1+", strict=True)
def test_looponfail_removed_test(self, pytester: pytest.Pytester) -> None:
modcol = pytester.getmodulecol(
textwrap.dedent(
"""
def test_one():
assert 0
def test_two():
assert 0
"""
)
)
remotecontrol = RemoteControl(modcol.config)
remotecontrol.loop_once()
assert len(remotecontrol.failures) == 2
modcol.fspath.write(py.code.Source("""
def test_xxx(): # renamed test
assert 0
def test_two():
assert 1 # pass now
"""))
removepyc(modcol.fspath)
modcol.path.write_text(
textwrap.dedent(
"""
def test_xxx(): # renamed test
assert 0
def test_two():
assert 1 # pass now
"""
)
)
removepyc(modcol.path)
remotecontrol.loop_once()
assert len(remotecontrol.failures) == 0
remotecontrol.loop_once()
assert len(remotecontrol.failures) == 1
def test_looponfail_multiple_errors(self, testdir, monkeypatch):
modcol = testdir.getmodulecol("""
def test_one():
assert 0
""")
def test_looponfail_multiple_errors(
self, pytester: pytest.Pytester, monkeypatch: pytest.MonkeyPatch
) -> None:
modcol = pytester.getmodulecol(
textwrap.dedent(
"""
def test_one():
assert 0
"""
)
)
remotecontrol = RemoteControl(modcol.config)
orig_runsession = remotecontrol.runsession
def runsession_dups():
def runsession_dups() -> tuple[list[str], list[str], bool]:
# twisted.trial test cases may report multiple errors.
failures, reports, collection_failed = orig_runsession()
print(failures)
return failures * 2, reports, collection_failed
monkeypatch.setattr(remotecontrol, 'runsession', runsession_dups)
monkeypatch.setattr(remotecontrol, "runsession", runsession_dups)
remotecontrol.loop_once()
assert len(remotecontrol.failures) == 1
class TestFunctional:
def test_fail_to_ok(self, testdir):
p = testdir.makepyfile("""
def test_one():
x = 0
assert x == 1
""")
# p = testdir.mkdir("sub").join(p1.basename)
def test_fail_to_ok(self, pytester: pytest.Pytester) -> None:
p = pytester.makepyfile(
textwrap.dedent(
"""
def test_one():
x = 0
assert x == 1
"""
)
)
# p = pytester.mkdir("sub").join(p1.basename)
# 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("x == 1")
child.expect("1 failed")
child.expect("### LOOPONFAILING ####")
child.expect("waiting for changes")
p.write(py.code.Source("""
def test_one():
x = 1
assert x == 1
"""))
p.write_text(
textwrap.dedent(
"""
def test_one():
x = 1
assert x == 1
"""
),
)
child.expect(".*1 passed.*")
child.kill(15)
def test_xfail_passes(self, testdir):
p = testdir.makepyfile("""
import py
@py.test.mark.xfail
def test_one():
pass
""")
child = testdir.spawn_pytest("-f %s" % p)
def test_xfail_passes(self, pytester: pytest.Pytester) -> None:
p = pytester.makepyfile(
textwrap.dedent(
"""
import pytest
@pytest.mark.xfail
def test_one():
pass
"""
)
)
child = pytester.spawn_pytest("-f %s" % p, expect_timeout=30.0)
child.expect("1 xpass")
child.expect("### LOOPONFAILING ####")
# child.expect("### LOOPONFAILING ####")
child.expect("waiting for changes")
child.kill(15)
def removepyc(path):
def removepyc(path: Path) -> None:
# XXX damn those pyc files
pyc = path + "c"
if pyc.check():
pyc.remove()
c = path.dirpath("__pycache__")
if c.check():
c.remove()
pyc = path.with_suffix(".pyc")
if pyc.exists():
pyc.unlink()
c = path.parent / "__pycache__"
if c.exists():
shutil.rmtree(c)

View File

@@ -2,54 +2,120 @@ import pytest
class TestHooks:
@pytest.fixture(autouse=True)
def create_test_file(self, testdir):
testdir.makepyfile("""
def create_test_file(self, pytester: pytest.Pytester) -> None:
pytester.makepyfile(
"""
import os
def test_a(): pass
def test_b(): pass
def test_c(): pass
""")
def test_runtest_logreport(self, testdir):
"""Test that log reports from pytest_runtest_logreport when running
with xdist contain "node", "nodeid" and "worker_id" attributes. (#8)
"""
testdir.makeconftest("""
)
def test_runtest_logreport(self, pytester: pytest.Pytester) -> None:
"""Test that log reports from pytest_runtest_logreport when running with
xdist contain "node", "nodeid", "worker_id", and "testrun_uid"
attributes (#8)."""
pytester.makeconftest(
"""
def pytest_runtest_logreport(report):
if hasattr(report, 'node'):
if report.when == "call":
slaveid = report.node.slaveinput['slaveid']
if slaveid != report.worker_id:
workerid = report.node.workerinput['workerid']
testrunuid = report.node.workerinput['testrunuid']
if workerid != report.worker_id:
print("HOOK: Worker id mismatch: %s %s"
% (slaveid, report.worker_id))
% (workerid, report.worker_id))
elif testrunuid != report.testrun_uid:
print("HOOK: Testrun uid mismatch: %s %s"
% (testrunuid, report.testrun_uid))
else:
print("HOOK: %s %s"
% (report.nodeid, report.worker_id))
""")
res = testdir.runpytest('-n1', '-s')
res.stdout.fnmatch_lines([
'*HOOK: test_runtest_logreport.py::test_a gw0*',
'*HOOK: test_runtest_logreport.py::test_b gw0*',
'*HOOK: test_runtest_logreport.py::test_c gw0*',
'*3 passed*',
])
def test_node_collection_finished(self, testdir):
"""Test pytest_xdist_node_collection_finished hook (#8).
print("HOOK: %s %s %s"
% (report.nodeid, report.worker_id, report.testrun_uid))
"""
testdir.makeconftest("""
)
res = pytester.runpytest("-n1", "-s")
res.stdout.fnmatch_lines(
[
"*HOOK: test_runtest_logreport.py::test_a gw0 *",
"*HOOK: test_runtest_logreport.py::test_b gw0 *",
"*HOOK: test_runtest_logreport.py::test_c gw0 *",
"*3 passed*",
]
)
def test_node_collection_finished(self, pytester: pytest.Pytester) -> None:
"""Test pytest_xdist_node_collection_finished hook (#8)."""
pytester.makeconftest(
"""
def pytest_xdist_node_collection_finished(node, ids):
slaveid = node.slaveinput['slaveid']
workerid = node.workerinput['workerid']
stripped_ids = [x.split('::')[1] for x in ids]
print("HOOK: %s %s" % (slaveid, ', '.join(stripped_ids)))
""")
res = testdir.runpytest('-n2', '-s')
res.stdout.fnmatch_lines_random([
'*HOOK: gw0 test_a, test_b, test_c',
'*HOOK: gw1 test_a, test_b, test_c',
])
res.stdout.fnmatch_lines([
'*3 passed*',
])
print("HOOK: %s %s" % (workerid, ', '.join(stripped_ids)))
"""
)
res = pytester.runpytest("-n2", "-s")
res.stdout.fnmatch_lines_random(
["*HOOK: gw0 test_a, test_b, test_c", "*HOOK: gw1 test_a, test_b, test_c"]
)
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,87 +1,339 @@
import py
from contextlib import suppress
import os
from pathlib import Path
import sys
import execnet
from xdist.slavemanage import NodeManager
import pytest
from xdist.workermanage import NodeManager
def test_dist_incompatibility_messages(testdir):
result = testdir.runpytest("--pdb", "--looponfail")
@pytest.fixture
def monkeypatch_3_cpus(monkeypatch: pytest.MonkeyPatch) -> None:
"""Make pytest-xdist believe the system has 3 CPUs."""
# block import
monkeypatch.setitem(sys.modules, "psutil", None)
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
result = testdir.runpytest("--pdb", "-n", "3")
result = pytester.runpytest("--pdb", "-n", "3")
assert result.ret != 0
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 "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
config = testdir.parseconfigure("-n 2")
config = pytester.parseconfigure("-n 2")
check_options(config)
assert config.option.dist == "load"
assert config.option.tx == ['popen'] * 2
config = testdir.parseconfigure("-d")
assert config.option.tx == ["popen"] * 2
config = pytester.parseconfigure("--numprocesses", "2")
check_options(config)
assert config.option.dist == "load"
assert config.option.tx == ["popen"] * 2
config = pytester.parseconfigure("--numprocesses", "3", "--maxprocesses", "2")
check_options(config)
assert config.option.dist == "load"
assert config.option.tx == ["popen"] * 2
config = pytester.parseconfigure("-d")
check_options(config)
assert config.option.dist == "load"
config = pytester.parseconfigure("--numprocesses", "0")
check_options(config)
assert config.option.dist == "no"
assert config.option.tx == []
def test_auto_detect_cpus(testdir, monkeypatch):
import os
if hasattr(os, 'cpu_count'):
monkeypatch.setattr(os, 'cpu_count', lambda: 99)
config = pytester.parseconfigure("--numprocesses", "0", "-d")
check_options(config)
assert config.option.dist == "no"
assert config.option.tx == []
config = pytester.parseconfigure(
"--numprocesses", "0", "--dist", "each", "--tx", "2*popen"
)
check_options(config)
assert config.option.dist == "no"
assert config.option.tx == []
def test_auto_detect_cpus(
pytester: pytest.Pytester, monkeypatch: pytest.MonkeyPatch
) -> None:
from xdist.plugin import pytest_cmdline_main as check_options
monkeypatch.delenv("PYTEST_XDIST_AUTO_NUM_WORKERS", raising=False)
with suppress(ImportError):
import psutil
monkeypatch.setattr(psutil, "cpu_count", lambda logical=True: None)
if hasattr(os, "sched_getaffinity"):
monkeypatch.setattr(os, "sched_getaffinity", lambda _pid: set(range(99)))
elif hasattr(os, "cpu_count"):
monkeypatch.setattr(os, "cpu_count", lambda: 99)
else:
import multiprocessing
monkeypatch.setattr(multiprocessing, 'cpu_count', lambda: 99)
config = testdir.parseconfigure("-n2")
assert config.getoption('numprocesses') == 2
monkeypatch.setattr(multiprocessing, "cpu_count", lambda: 99)
config = testdir.parseconfigure("-nauto")
assert config.getoption('numprocesses') == 99
config = pytester.parseconfigure("-n2")
assert config.getoption("numprocesses") == 2
config = pytester.parseconfigure("-nauto")
check_options(config)
assert config.getoption("numprocesses") == 99
for numprocesses in (0, "auto", "logical"):
config = pytester.parseconfigure(f"-n{numprocesses}", "--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.setenv("TRAVIS", "true")
config = pytester.parseconfigure("-nauto")
check_options(config)
assert config.getoption("numprocesses") == 2
def test_auto_detect_cpus_psutil(
pytester: pytest.Pytester, monkeypatch: pytest.MonkeyPatch
) -> None:
from xdist.plugin import pytest_cmdline_main as check_options
psutil = pytest.importorskip("psutil")
monkeypatch.delenv("PYTEST_XDIST_AUTO_NUM_WORKERS", raising=False)
monkeypatch.setattr(psutil, "cpu_count", lambda logical=True: 84 if logical else 42)
config = pytester.parseconfigure("-nauto")
check_options(config)
assert config.getoption("numprocesses") == 42
config = pytester.parseconfigure("-nlogical")
check_options(config)
assert config.getoption("numprocesses") == 84
def test_auto_detect_cpus_os(
pytester: pytest.Pytester, monkeypatch: pytest.MonkeyPatch, monkeypatch_3_cpus: None
) -> None:
from xdist.plugin import pytest_cmdline_main as check_options
monkeypatch.delenv("PYTEST_XDIST_AUTO_NUM_WORKERS", raising=False)
config = pytester.parseconfigure("-nauto")
check_options(config)
assert config.getoption("numprocesses") == 3
config = pytester.parseconfigure("-nlogical")
check_options(config)
assert config.getoption("numprocesses") == 3
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
) -> 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
monkeypatch.delenv("PYTEST_XDIST_AUTO_NUM_WORKERS", raising=False)
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
) -> 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
) -> 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_configure as configure
config = pytester.parseconfigure("-n1")
check_options(config)
configure(config)
assert config.pluginmanager.hasplugin("dsession")
config = pytester.parseconfigure("-n1", "--collect-only")
check_options(config)
configure(config)
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:
def test_getxspecs(self, testdir):
config = testdir.parseconfigure("--tx=popen", "--tx", "ssh=xyz")
def test_getxspecs(self, pytester: pytest.Pytester) -> None:
config = pytester.parseconfigure("--tx=popen", "--tx", "ssh=xyz")
nodemanager = NodeManager(config)
xspecs = nodemanager._getxspecs()
xspecs = nodemanager._gettxspecs()
assert len(xspecs) == 2
print(xspecs)
assert xspecs[0].popen
assert xspecs[1].ssh == "xyz"
def test_xspecs_multiplied(self, testdir):
config = testdir.parseconfigure("--tx=3*popen", )
xspecs = NodeManager(config)._getxspecs()
def test_xspecs_multiplied(self, pytester: pytest.Pytester) -> None:
config = pytester.parseconfigure("--tx=3*popen")
xspecs = NodeManager(config)._gettxspecs()
assert len(xspecs) == 3
assert xspecs[1].popen
def test_getrsyncdirs(self, testdir):
config = testdir.parseconfigure('--rsyncdir=' + str(testdir.tmpdir))
def test_getrsyncdirs(self, pytester: pytest.Pytester) -> None:
config = pytester.parseconfigure("--rsyncdir=" + str(pytester.path))
nm = NodeManager(config, specs=[execnet.XSpec("popen")])
assert not nm._getrsyncdirs()
nm = NodeManager(config, specs=[execnet.XSpec("popen//chdir=qwe")])
assert nm.roots
assert testdir.tmpdir in nm.roots
assert pytester.path in nm.roots
def test_getrsyncignore(self, testdir):
config = testdir.parseconfigure('--rsyncignore=fo*')
def test_getrsyncignore(self, pytester: pytest.Pytester) -> None:
config = pytester.parseconfigure("--rsyncignore=fo*")
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):
p = py.path.local()
for bn in 'x y z'.split():
p.mkdir(bn)
testdir.makeini("""
def test_getrsyncdirs_with_conftest(self, pytester: pytest.Pytester) -> None:
p = Path.cwd()
for bn in ("x", "y", "z"):
p.joinpath(bn).mkdir()
pytester.makeini(
"""
[pytest]
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")])
roots = nm._getrsyncdirs()
# assert len(roots) == 3 + 1 # pylib
assert py.path.local('y') in roots
assert py.path.local('z') in roots
assert testdir.tmpdir.join('x') in roots
assert Path("y").resolve() in roots
assert Path("z").resolve() in roots
assert pytester.path.joinpath("x") in roots

View File

@@ -1,55 +1,73 @@
import py
import pytest
from xdist.slavemanage import SlaveController, unserialize_report
from xdist.remote import serialize_report
import execnet
import marshal
from __future__ import annotations
import marshal
import pprint
from queue import Queue
import sys
from typing import Any
from typing import Callable
from typing import cast
from typing import Union
import uuid
import execnet
import pytest
from xdist.workermanage import NodeManager
from xdist.workermanage import WorkerController
queue = py.builtin._tryimport("queue", "Queue")
WAIT_TIMEOUT = 10.0
def check_marshallable(d):
def check_marshallable(d: object) -> None:
try:
marshal.dumps(d)
except ValueError:
py.std.pprint.pprint(d)
raise ValueError("not marshallable")
marshal.dumps(d) # type: ignore[arg-type]
except ValueError as e:
pprint.pprint(d)
raise ValueError("not marshallable") from e
class EventCall:
def __init__(self, eventcall):
def __init__(self, eventcall: tuple[str, dict[str, Any]]) -> None:
self.name, self.kwargs = eventcall
def __str__(self):
return "<EventCall %s(**%s)>" % (self.name, self.kwargs)
def __str__(self) -> str:
return f"<EventCall {self.name}(**{self.kwargs})>"
class SlaveSetup:
use_callback = False
def __init__(self, request):
self.testdir = request.getfuncargvalue("testdir")
class WorkerSetup:
def __init__(
self, request: pytest.FixtureRequest, pytester: pytest.Pytester
) -> None:
self.request = request
self.events = queue.Queue()
self.pytester = pytester
self.use_callback = False
self.events = Queue() # type: ignore[var-annotated]
def setup(self, ):
self.testdir.chdir()
def setup(self) -> None:
self.pytester.chdir()
# import os ; os.environ['EXECNET_DEBUG'] = "2"
self.gateway = execnet.makegateway()
self.config = config = self.testdir.parseconfigure()
putevent = self.use_callback and self.events.put or None
self.gateway = execnet.makegateway("execmodel=main_thread_only//popen")
self.config = config = self.pytester.parseconfigure()
putevent = self.events.put if self.use_callback else None
class DummyMananger:
testrunuid = uuid.uuid4().hex
specs = [0, 1]
self.slp = SlaveController(DummyMananger, self.gateway, config,
putevent)
nodemanager = cast(NodeManager, DummyMananger)
self.slp = WorkerController(
nodemanager=nodemanager,
gateway=self.gateway,
config=config,
putevent=putevent, # type: ignore[arg-type]
)
self.request.addfinalizer(self.slp.ensure_teardown)
self.slp.setup()
def popevent(self, name=None):
def popevent(self, name: str | None = None) -> EventCall:
while 1:
if self.use_callback:
data = self.events.get(timeout=WAIT_TIMEOUT)
@@ -58,218 +76,346 @@ class SlaveSetup:
ev = EventCall(data)
if name is None or ev.name == name:
return ev
print("skipping %s" % (ev, ))
print(f"skipping {ev}")
def sendcommand(self, name, **kwargs):
def sendcommand(self, name: str, **kwargs: Any) -> None:
self.slp.sendcommand(name, **kwargs)
@pytest.fixture
def slave(request):
return SlaveSetup(request)
def worker(request: pytest.FixtureRequest, pytester: pytest.Pytester) -> WorkerSetup:
return WorkerSetup(request, pytester)
@pytest.mark.xfail(reason='#59')
def test_remoteinitconfig(testdir):
from xdist.remote import remote_initconfig
config1 = testdir.parseconfig()
config2 = remote_initconfig(config1.option.__dict__, config1.args)
assert config2.option.__dict__ == config1.option.__dict__
assert config2.pluginmanager.getplugin("terminal") in (-1, None)
class TestWorkerInteractor:
UnserializerReport = Callable[
[dict[str, Any]], Union[pytest.CollectReport, pytest.TestReport]
]
@pytest.fixture
def unserialize_report(self, pytestconfig: pytest.Config) -> UnserializerReport:
def unserialize(
data: dict[str, Any],
) -> pytest.CollectReport | pytest.TestReport:
return pytestconfig.hook.pytest_report_from_serializable( # type: ignore[no-any-return]
config=pytestconfig, data=data
)
class TestReportSerialization:
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.longrepr == str(rep.longrepr)
return unserialize
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 TestSlaveInteractor:
def test_basic_collect_and_runtests(self, slave):
slave.testdir.makepyfile("""
def test_basic_collect_and_runtests(
self, worker: WorkerSetup, unserialize_report: UnserializerReport
) -> None:
worker.pytester.makepyfile(
"""
def test_func():
pass
""")
slave.setup()
ev = slave.popevent()
assert ev.name == "slaveready"
ev = slave.popevent()
"""
)
worker.setup()
ev = worker.popevent()
assert ev.name == "workerready"
ev = worker.popevent()
assert ev.name == "collectionstart"
assert not ev.kwargs
ev = slave.popevent("collectionfinish")
assert ev.kwargs['topdir'] == slave.testdir.tmpdir
ids = ev.kwargs['ids']
ev = worker.popevent("collectionfinish")
assert ev.kwargs["topdir"] == str(worker.pytester.path)
ids = ev.kwargs["ids"]
assert len(ids) == 1
slave.sendcommand("runtests", indices=list(range(len(ids))))
slave.sendcommand("shutdown")
ev = slave.popevent("logstart")
worker.sendcommand("runtests", indices=list(range(len(ids))))
worker.sendcommand("shutdown")
ev = worker.popevent("logstart")
assert ev.kwargs["nodeid"].endswith("test_func")
assert len(ev.kwargs["location"]) == 3
ev = slave.popevent("testreport") # setup
ev = slave.popevent("testreport")
ev = worker.popevent("testreport") # setup
ev = worker.popevent("testreport")
assert ev.name == "testreport"
rep = unserialize_report(ev.name, ev.kwargs['data'])
rep = unserialize_report(ev.kwargs["data"])
assert rep.nodeid.endswith("::test_func")
assert rep.passed
assert rep.when == "call"
ev = slave.popevent("slavefinished")
assert 'slaveoutput' in ev.kwargs
ev = worker.popevent("workerfinished")
assert "workeroutput" in ev.kwargs
@pytest.mark.skipif(pytest.__version__ >= '3.0',
reason='skip at module level illegal in pytest 3.0')
def test_remote_collect_skip(self, slave):
slave.testdir.makepyfile("""
import py
py.test.skip("hello")
""")
slave.setup()
ev = slave.popevent("collectionstart")
def test_remote_collect_skip(
self, worker: WorkerSetup, unserialize_report: UnserializerReport
) -> None:
worker.pytester.makepyfile(
"""
import pytest
pytest.skip("hello", allow_module_level=True)
"""
)
worker.setup()
ev = worker.popevent("collectionstart")
assert not ev.kwargs
ev = slave.popevent()
ev = worker.popevent()
assert ev.name == "collectreport"
ev = slave.popevent()
assert ev.name == "collectreport"
rep = unserialize_report(ev.name, ev.kwargs['data'])
rep = unserialize_report(ev.kwargs["data"])
assert rep.skipped
ev = slave.popevent("collectionfinish")
assert not ev.kwargs['ids']
assert isinstance(rep.longrepr, tuple)
assert rep.longrepr[2] == "Skipped: hello"
ev = worker.popevent("collectionfinish")
assert not ev.kwargs["ids"]
def test_remote_collect_fail(self, slave):
slave.testdir.makepyfile("""aasd qwe""")
slave.setup()
ev = slave.popevent("collectionstart")
def test_remote_collect_fail(
self, worker: WorkerSetup, unserialize_report: UnserializerReport
) -> None:
worker.pytester.makepyfile("""aasd qwe""")
worker.setup()
ev = worker.popevent("collectionstart")
assert not ev.kwargs
ev = slave.popevent()
ev = worker.popevent()
assert ev.name == "collectreport"
ev = slave.popevent()
assert ev.name == "collectreport"
rep = unserialize_report(ev.name, ev.kwargs['data'])
rep = unserialize_report(ev.kwargs["data"])
assert rep.failed
ev = slave.popevent("collectionfinish")
assert not ev.kwargs['ids']
ev = worker.popevent("collectionfinish")
assert not ev.kwargs["ids"]
def test_runtests_all(self, slave):
slave.testdir.makepyfile("""
def test_runtests_all(
self, worker: WorkerSetup, unserialize_report: UnserializerReport
) -> None:
worker.pytester.makepyfile(
"""
def test_func(): pass
def test_func2(): pass
""")
slave.setup()
ev = slave.popevent()
assert ev.name == "slaveready"
ev = slave.popevent()
"""
)
worker.setup()
ev = worker.popevent()
assert ev.name == "workerready"
ev = worker.popevent()
assert ev.name == "collectionstart"
assert not ev.kwargs
ev = slave.popevent("collectionfinish")
ids = ev.kwargs['ids']
ev = worker.popevent("collectionfinish")
ids = ev.kwargs["ids"]
assert len(ids) == 2
slave.sendcommand("runtests_all", )
slave.sendcommand("shutdown", )
worker.sendcommand("runtests_all")
worker.sendcommand("shutdown")
for func in "::test_func", "::test_func2":
for i in range(3): # setup/call/teardown
ev = slave.popevent("testreport")
for _ in range(3): # setup/call/teardown
ev = worker.popevent("testreport")
assert ev.name == "testreport"
rep = unserialize_report(ev.name, ev.kwargs['data'])
rep = unserialize_report(ev.kwargs["data"])
assert rep.nodeid.endswith(func)
ev = slave.popevent("slavefinished")
assert 'slaveoutput' in ev.kwargs
ev = worker.popevent("workerfinished")
assert "workeroutput" in ev.kwargs
def test_happy_run_events_converted(self, testdir, slave):
py.test.xfail("implement a simple test for event production")
assert not slave.use_callback
slave.testdir.makepyfile("""
def test_happy_run_events_converted(
self, pytester: pytest.Pytester, worker: WorkerSetup
) -> None:
pytest.xfail("implement a simple test for event production")
assert not worker.use_callback # type: ignore[unreachable]
worker.pytester.makepyfile(
"""
def test_func():
pass
""")
slave.setup()
hookrec = testdir.getreportrecorder(slave.config)
for data in slave.slp.channel:
slave.slp.process_from_remote(data)
slave.slp.process_from_remote(slave.slp.ENDMARK)
py.std.pprint.pprint(hookrec.hookrecorder.calls)
hookrec.hookrecorder.contains([
("pytest_collectstart", "collector.fspath == aaa"),
("pytest_pycollect_makeitem", "name == 'test_func'"),
("pytest_collectreport", "report.collector.fspath == aaa"),
("pytest_collectstart", "collector.fspath == bbb"),
("pytest_pycollect_makeitem", "name == 'test_func'"),
("pytest_collectreport", "report.collector.fspath == bbb"),
])
"""
)
worker.setup()
hookrec = pytester.getreportrecorder(worker.config)
for data in worker.slp.channel:
worker.slp.process_from_remote(data)
worker.slp.process_from_remote(worker.slp.ENDMARK)
pprint.pprint(hookrec.hookrecorder.calls)
hookrec.hookrecorder.contains(
[
("pytest_collectstart", "collector.fspath == aaa"),
("pytest_pycollect_makeitem", "name == 'test_func'"),
("pytest_collectreport", "report.collector.fspath == aaa"),
("pytest_collectstart", "collector.fspath == bbb"),
("pytest_pycollect_makeitem", "name == 'test_func'"),
("pytest_collectreport", "report.collector.fspath == bbb"),
]
)
def test_process_from_remote_error_handling(
self, worker: WorkerSetup, capsys: pytest.CaptureFixture[str]
) -> None:
worker.use_callback = True
worker.setup()
worker.slp.process_from_remote(("<nonono>", {}))
out, err = capsys.readouterr()
assert "INTERNALERROR> ValueError: unknown event: <nonono>" in out
ev = worker.popevent()
assert ev.name == "errordown"
def test_steal_work(
self, worker: WorkerSetup, unserialize_report: UnserializerReport
) -> None:
worker.pytester.makepyfile(
"""
import time
def test_func(): time.sleep(1)
def test_func2(): pass
def test_func3(): pass
def test_func4(): pass
"""
)
worker.setup()
ev = worker.popevent("collectionfinish")
ids = ev.kwargs["ids"]
assert len(ids) == 4
worker.sendcommand("runtests_all")
# wait for test_func setup
ev = worker.popevent("testreport")
rep = unserialize_report(ev.kwargs["data"])
assert rep.nodeid.endswith("::test_func")
assert rep.when == "setup"
worker.sendcommand("steal", indices=[1, 2])
ev = worker.popevent("unscheduled")
# Cannot steal index 1 because it is completed already, so do not steal any.
assert ev.kwargs["indices"] == []
# Index 2 can be stolen, as it is still pending.
worker.sendcommand("steal", indices=[2])
ev = worker.popevent("unscheduled")
assert ev.kwargs["indices"] == [2]
reports = [
("test_func", "call"),
("test_func", "teardown"),
("test_func2", "setup"),
("test_func2", "call"),
("test_func2", "teardown"),
]
for func, when in reports:
ev = worker.popevent("testreport")
rep = unserialize_report(ev.kwargs["data"])
assert rep.nodeid.endswith(f"::{func}")
assert rep.when == when
worker.sendcommand("shutdown")
for when in ["setup", "call", "teardown"]:
ev = worker.popevent("testreport")
rep = unserialize_report(ev.kwargs["data"])
assert rep.nodeid.endswith("::test_func4")
assert rep.when == when
ev = worker.popevent("workerfinished")
assert "workeroutput" in ev.kwargs
def test_steal_empty_queue(
self, worker: WorkerSetup, unserialize_report: UnserializerReport
) -> None:
worker.pytester.makepyfile(
"""
def test_func(): pass
def test_func2(): pass
"""
)
worker.setup()
ev = worker.popevent("collectionfinish")
ids = ev.kwargs["ids"]
assert len(ids) == 2
worker.sendcommand("runtests_all")
for when in ["setup", "call", "teardown"]:
ev = worker.popevent("testreport")
rep = unserialize_report(ev.kwargs["data"])
assert rep.nodeid.endswith("::test_func")
assert rep.when == when
worker.sendcommand("steal", indices=[0, 1])
ev = worker.popevent("unscheduled")
assert ev.kwargs["indices"] == []
worker.sendcommand("shutdown")
for when in ["setup", "call", "teardown"]:
ev = worker.popevent("testreport")
rep = unserialize_report(ev.kwargs["data"])
assert rep.nodeid.endswith("::test_func2")
assert rep.when == when
ev = worker.popevent("workerfinished")
assert "workeroutput" in ev.kwargs
def test_remote_env_vars(testdir):
testdir.makepyfile('''
def test_remote_env_vars(pytester: pytest.Pytester) -> None:
pytester.makepyfile(
"""
import os
def test():
assert len(os.environ['PYTEST_XDIST_TESTRUNUID']) == 32
assert os.environ['PYTEST_XDIST_WORKER'] in ('gw0', 'gw1')
assert os.environ['PYTEST_XDIST_WORKER_COUNT'] == '2'
''')
result = testdir.runpytest('-n2', '--max-slave-restart=0')
"""
)
result = pytester.runpytest("-n2", "--max-worker-restart=0")
assert result.ret == 0
def test_remote_inner_argv(pytester: pytest.Pytester) -> None:
"""Test/document the behavior due to execnet using `python -c`."""
pytester.makepyfile(
"""
import sys
def test_argv():
assert sys.argv == ["-c"]
"""
)
result = pytester.runpytest("-n1")
assert result.ret == 0
def test_remote_mainargv(pytester: pytest.Pytester) -> None:
outer_argv = sys.argv
pytester.makepyfile(
f"""
def test_mainargv(request):
assert request.config.workerinput["mainargv"] == {outer_argv!r}
"""
)
result = pytester.runpytest("-n1")
assert result.ret == 0
def test_remote_usage_prog(pytester: pytest.Pytester) -> None:
pytester.makeconftest(
"""
import pytest
config_parser = None
@pytest.fixture
def get_config_parser():
return config_parser
def pytest_configure(config):
global config_parser
config_parser = config._parser
"""
)
pytester.makepyfile(
"""
import sys
def test(get_config_parser, request):
get_config_parser._getparser().error("my_usage_error")
"""
)
result = pytester.runpytest_subprocess("-n1")
assert result.ret == 1
result.stdout.fnmatch_lines(["*usage: *", "*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,257 +0,0 @@
import py
import pytest
import execnet
from _pytest.pytester import HookRecorder
from xdist import slavemanage, newhooks
from xdist.slavemanage 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 slavecontroller(monkeypatch):
class MockController(object):
def __init__(self, *args):
pass
def setup(self):
pass
monkeypatch.setattr(slavemanage, 'SlaveController', 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):
l = ["ssh=noco", "socket=xyz"]
for spec in NodeManager(config, l).specs:
assert spec.chdir == "pyexecnetcache"
for spec in NodeManager(config, l, defaultchdir="abc").specs:
assert spec.chdir == "abc"
def test_popen_makegateway_events(self, config, hookrecorder,
slavecontroller):
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, slavecontroller):
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
l = []
for gw in hm.group:
hm.rsync(gw, source, notify=lambda *args: l.append(args))
assert not l
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, slavecontroller):
source, dest = mysetup.source, mysetup.dest
hm = NodeManager(config, ["popen//chdir=%s" % dest] * 1)
hm.setup_nodes(None)
source.ensure("dir1", "dir2", "hello")
l = []
for gw in hm.group:
hm.rsync(gw, source, notify=lambda *args: l.append(args))
assert len(l) == 1
assert l[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,
slavecontroller):
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)
l = list(source.visit(rec=syncer.filter, fil=syncer.filter))
assert len(l) == 3
basenames = [x.basename for x in l]
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)
py.builtin.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, slavecontroller):
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, slavecontroller):
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(py.std.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, slavecontroller):
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(py.std.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, slavecontroller):
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,472 @@
from __future__ import annotations
from pathlib import Path
import shutil
import textwrap
import warnings
import execnet
import pytest
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
from xdist.workermanage import NodeManager
from xdist.workermanage import unserialize_warning_message
pytest_plugins = "pytester"
@pytest.fixture
def hookrecorder(
config: pytest.Config, pytester: pytest.Pytester
) -> pytest.HookRecorder:
hookrecorder = pytester.make_hook_recorder(config.pluginmanager)
return hookrecorder
@pytest.fixture
def config(pytester: pytest.Pytester) -> pytest.Config:
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) -> None:
class MockController:
def __init__(self, *args: object) -> None:
pass
def setup(self) -> None:
pass
monkeypatch.setattr(workermanage, "WorkerController", MockController)
class TestNodeManagerPopen:
def test_popen_no_default_chdir(self, config: pytest.Config) -> None:
gm = NodeManager(config, ["popen"])
assert gm.specs[0].chdir is None
def test_default_chdir(self, config: pytest.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: pytest.Config,
hookrecorder: pytest.HookRecorder,
workercontroller: None,
) -> None:
hm = NodeManager(config, ["popen"] * 2)
hm.setup_nodes(None) # type: ignore[arg-type]
call = hookrecorder.popcall("pytest_xdist_setupnodes")
assert len(call.specs) == 2
call = hookrecorder.popcall("pytest_xdist_newgateway")
assert call.gateway.spec == execnet.XSpec("execmodel=main_thread_only//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: pytest.Config,
source: Path,
dest: Path,
workercontroller: None,
) -> None:
hm = NodeManager(config, ["popen"] * 2)
hm.setup_nodes(None) # type: ignore[arg-type]
assert len(hm.group) == 2
for gw in hm.group:
class pseudoexec:
args = [] # type: ignore[var-annotated]
def __init__(self, *args: object) -> None:
self.args.extend(args)
def waitclose(self) -> None:
pass
gw.remote_exec = pseudoexec # type: ignore[assignment]
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] # type: ignore[attr-defined]
def test_rsync_popen_with_path(
self, config: pytest.Config, source: Path, dest: Path, workercontroller: None
) -> None:
hm = NodeManager(config, ["popen//chdir=%s" % dest] * 1)
hm.setup_nodes(None) # type: ignore[arg-type]
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: pytest.Config,
source: Path,
dest: Path,
hookrecorder: pytest.HookRecorder,
workercontroller: None,
) -> None:
hm = NodeManager(config, ["popen//chdir=%s" % dest] * 2)
hm.roots = []
hm.setup_nodes(None) # type: ignore[arg-type]
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("execmodel=main_thread_only//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").is_dir()
assert p.joinpath("dir1", "file1").is_file()
def test_popen_rsync_subdir(
self,
pytester: pytest.Pytester,
source: Path,
dest: Path,
workercontroller: None,
) -> 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
)
)
# calls .rsync_roots()
nodemanager.setup_nodes(None) # type: ignore[arg-type]
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: None,
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))
# calls .rsync_roots()
nodemanager.setup_nodes(None) # type: ignore[arg-type]
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,
) -> 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])
# calls .rsync_roots()
nodemanager.setup_nodes(None) # type: ignore[arg-type]
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,
) -> 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])
# calls .rsync_roots()
nodemanager.setup_nodes(None) # type: ignore[arg-type]
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,
) -> None:
specs = ["popen"] * 3
source.joinpath("conftest.py").write_text("rsyncdirs = ['a']")
source.joinpath("a").mkdir()
config = pytester.parseconfig(source)
nodemanager = NodeManager(config, specs)
# calls .rysnc_roots()
nodemanager.setup_nodes(None) # type: ignore[arg-type]
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
def test_proxy_gateway_setup_nodes(self, pytester: pytest.Pytester) -> None:
nodemanager = NodeManager(
pytester.parseconfig(
"--px", "popen//id=my_proxy", "--tx", "popen//via=my_proxy"
)
)
nodes = nodemanager.setup_nodes(None) # type: ignore[arg-type]
# Proxy gateways are considered as an execnet gateway
assert len(nodemanager.group) == 2
# Proxy gateways do not run workers
assert len(nodes) == 1
def test_proxy_gateway(self, pytester: pytest.Pytester) -> None:
pytester.makepyfile(
__init__="",
test_x="""
def test_one():
pass
""",
)
reprec = pytester.inline_run(
"-d", "--px", "popen//id=my_proxy", "--tx", "popen//via=my_proxy"
)
rep = reprec.getreports("pytest_runtest_logreport")
assert rep[1].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: type[Warning] | str) -> None:
"""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) is 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() -> None:
"""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() -> None:
warnings.warn(MyWarning2("hello"))

77
tox.ini
View File

@@ -1,43 +1,50 @@
[tox]
# if you change the envlist, please update .travis.yml file as well
envlist=
py{26,27,34,35,36}-pytest{27,28,29,30}
py{27,35}-pytest28-pexpect
flakes
readme
envlist =
linting
py{39,310,311,312,313}-pytestlatest
py310-pytestmain
py310-psutil
py310-setproctitle
isolated_build = true
[testenv]
changedir=testing
passenv = USER USERNAME
extras =
testing
psutil: psutil
setproctitle: setproctitle
deps =
pycmd
# to avoid .eggs
setuptools_scm
pytest27: pytest~=2.7.2
pytest28: pytest~=2.8.7
pytest29: pytest~=2.9.2
pytest30: pytest~=3.0.5
pexpect: pexpect
platform=
pexpect: linux|darwin
commands=
# always clean to avoid code unmarshal mismatch on old python/pytest
py.cleanup -aq
py.test {posargs}
pytestmin: pytest==7.0.0
pytestlatest: pytest
pytestmain: git+https://github.com/pytest-dev/pytest.git
commands =
pytest {posargs:{env:_XDIST_TOX_DEFAULT_POSARGS:}}
setenv =
_XDIST_TOX_DEFAULT_POSARGS={env:_XDIST_TOX_POSARGS_PSUTIL:}
psutil: _XDIST_TOX_POSARGS_PSUTIL=-k psutil
[testenv:flakes]
changedir=
deps = flake8
commands = flake8 setup.py testing xdist
[testenv:linting]
skip_install = True
usedevelop = True
passenv = PRE_COMMIT_HOME
deps =
pre-commit
commands = pre-commit run --all-files --show-diff-on-failure
[testenv:readme]
[testenv:release]
changedir =
deps = readme
skip_install = true
commands = python setup.py check -r -s
description = do a release, required posarg of the version number
skipsdist = True
usedevelop = True
passenv = *
deps =
towncrier
commands =
towncrier build --version {posargs} --yes
[pytest]
addopts = -rsfxX
;; hello
[testenv:docs]
usedevelop = True
deps =
sphinx
sphinx_rtd_theme
commands =
sphinx-build -W --keep-going -b html docs docs/_build/html {posargs:}

View File

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

View File

@@ -1,60 +0,0 @@
import py
def pytest_addoption(parser):
group = parser.getgroup("xdist", "distributed and subprocess testing")
group.addoption(
'--boxed',
action="store_true", dest="boxed", default=False,
help="box each test run in a separate process (unix)")
def pytest_runtest_protocol(item):
if item.config.getvalue("boxed"):
reports = forked_run_report(item)
for rep in reports:
item.ihook.pytest_runtest_logreport(report=rep)
return True
def forked_run_report(item):
# for now, we run setup/teardown in the subprocess
# XXX optionally allow sharing of setup/teardown
from _pytest.runner import runtestprotocol
EXITSTATUS_TESTEXIT = 4
import marshal
from xdist.remote import serialize_report
from xdist.slavemanage import unserialize_report
def runforked():
try:
reports = runtestprotocol(item, log=False)
except KeyboardInterrupt:
py.std.os._exit(EXITSTATUS_TESTEXIT)
return marshal.dumps([serialize_report(x) for x in reports])
ff = py.process.ForkedFunc(runforked)
result = ff.waitfinish()
if result.retval is not None:
report_dumps = marshal.loads(result.retval)
return [unserialize_report("testreport", x) for x in report_dumps]
else:
if result.exitstatus == EXITSTATUS_TESTEXIT:
py.test.exit("forked test item %s raised Exit" % (item,))
return [report_process_crash(item, result)]
def report_process_crash(item, result):
path, lineno = item._getfslineno()
info = ("%s:%s: running the test CRASHED with signal %d" %
(path, lineno, result.signal))
from _pytest import runner
call = runner.CallInfo(lambda: 0/0, "???")
call.excinfo = info
rep = runner.pytest_runtest_makereport(item, call)
if result.out:
rep.sections.append(("captured stdout", result.out))
if result.err:
rep.sections.append(("captured stderr", result.err))
return rep

View File

@@ -1,793 +0,0 @@
import difflib
import itertools
from _pytest.runner import CollectReport
import pytest
import py
from xdist.slavemanage import NodeManager, parse_spec_config
queue = py.builtin._tryimport('queue', 'Queue')
class EachScheduling:
"""Implement scheduling of test items on all nodes
If a node gets added after the test run is started then it is
assumed to replace a node which got removed before it finished
its collection. In this case it will only be used if a node
with the same spec got removed earlier.
Any nodes added after the run is started will only get items
assigned if a node with a matching spec was removed before it
finished all its pending items. The new node will then be
assigned the remaining items from the removed node.
"""
def __init__(self, config, log=None):
self.config = config
self.numnodes = len(parse_spec_config(config))
self.node2collection = {}
self.node2pending = {}
self._started = []
self._removed2pending = {}
if log is None:
self.log = py.log.Producer("eachsched")
else:
self.log = log.eachsched
self.collection_is_completed = False
@property
def nodes(self):
"""A list of all nodes in the scheduler."""
return list(self.node2pending.keys())
@property
def tests_finished(self):
if not self.collection_is_completed:
return False
if self._removed2pending:
return False
for pending in self.node2pending.values():
if len(pending) >= 2:
return False
return True
@property
def has_pending(self):
"""Return True if there are pending test items
This indicates that collection has finished and nodes are
still processing test items, so this can be thought of as
"the scheduler is active".
"""
for pending in self.node2pending.values():
if pending:
return True
return False
def add_node(self, node):
assert node not in self.node2pending
self.node2pending[node] = []
def add_node_collection(self, node, collection):
"""Add the collected test items from a node
Collection is complete once all nodes have submitted their
collection. In this case its pending list is set to an empty
list. When the collection is already completed this
submission is from a node which was restarted to replace a
dead node. In this case we already assign the pending items
here. In either case ``.schedule()`` will instruct the
node to start running the required tests.
"""
assert node in self.node2pending
if not self.collection_is_completed:
self.node2collection[node] = list(collection)
self.node2pending[node] = []
if len(self.node2collection) >= self.numnodes:
self.collection_is_completed = True
elif self._removed2pending:
for deadnode in self._removed2pending:
if deadnode.gateway.spec == node.gateway.spec:
dead_collection = self.node2collection[deadnode]
if collection != dead_collection:
msg = report_collection_diff(dead_collection,
collection,
deadnode.gateway.id,
node.gateway.id)
self.log(msg)
return
pending = self._removed2pending.pop(deadnode)
self.node2pending[node] = pending
break
def mark_test_complete(self, node, item_index, duration=0):
self.node2pending[node].remove(item_index)
def remove_node(self, node):
# KeyError if we didn't get an add_node() yet
pending = self.node2pending.pop(node)
if not pending:
return
crashitem = self.node2collection[node][pending.pop(0)]
if pending:
self._removed2pending[node] = pending
return crashitem
def schedule(self):
"""Schedule the test items on the nodes
If the node's pending list is empty it is a new node which
needs to run all the tests. If the pending list is already
populated (by ``.add_node_collection()``) then it replaces a
dead node and we only need to run those tests.
"""
assert self.collection_is_completed
for node, pending in self.node2pending.items():
if node in self._started:
continue
if not pending:
pending[:] = range(len(self.node2collection[node]))
node.send_runtest_all()
else:
node.send_runtest_some(pending)
self._started.append(node)
class LoadScheduling:
"""Implement load scheduling across nodes.
This distributes the tests collected across all nodes so each test
is run just once. All nodes collect and submit the test suite and
when all collections are received it is verified they are
identical collections. Then the collection gets divided up in
chunks and chunks get submitted to nodes. Whenever a node finishes
an item, it calls ``.mark_test_complete()`` which will trigger the
scheduler to assign more tests if the number of pending tests for
the node falls below a low-watermark.
When created, ``numnodes`` defines how many nodes are expected to
submit a collection. This is used to know when all nodes have
finished collection or how large the chunks need to be created.
Attributes:
:numnodes: The expected number of nodes taking part. The actual
number of nodes will vary during the scheduler's lifetime as
nodes are added by the DSession as they are brought up and
removed either because of a dead node or normal shutdown. This
number is primarily used to know when the initial collection is
completed.
:node2collection: Map of nodes and their test collection. All
collections should always be identical.
:node2pending: Map of nodes and the indices of their pending
tests. The indices are an index into ``.pending`` (which is
identical to their own collection stored in
``.node2collection``).
:collection: The one collection once it is validated to be
identical between all the nodes. It is initialised to None
until ``.schedule()`` is called.
:pending: List of indices of globally pending tests. These are
tests which have not yet been allocated to a chunk for a node
to process.
:log: A py.log.Producer instance.
:config: Config object, used for handling hooks.
"""
def __init__(self, config, log=None):
self.numnodes = len(parse_spec_config(config))
self.node2collection = {}
self.node2pending = {}
self.pending = []
self.collection = None
if log is None:
self.log = py.log.Producer("loadsched")
else:
self.log = log.loadsched
self.config = config
@property
def nodes(self):
"""A list of all nodes in the scheduler."""
return list(self.node2pending.keys())
@property
def collection_is_completed(self):
"""Boolean indication initial test collection is complete.
This is a boolean indicating all initial participating nodes
have finished collection. The required number of initial
nodes is defined by ``.numnodes``.
"""
return len(self.node2collection) >= self.numnodes
@property
def tests_finished(self):
"""Return True if all tests have been executed by the nodes."""
if not self.collection_is_completed:
return False
if self.pending:
return False
for pending in self.node2pending.values():
if len(pending) >= 2:
return False
return True
@property
def has_pending(self):
"""Return True if there are pending test items
This indicates that collection has finished and nodes are
still processing test items, so this can be thought of as
"the scheduler is active".
"""
if self.pending:
return True
for pending in self.node2pending.values():
if pending:
return True
return False
def add_node(self, node):
"""Add a new node to the scheduler.
From now on the node will be allocated chunks of tests to
execute.
Called by the ``DSession.slave_slaveready`` hook when it
successfully bootstraps a new node.
"""
assert node not in self.node2pending
self.node2pending[node] = []
def add_node_collection(self, node, collection):
"""Add the collected test items from a node
The collection is stored in the ``.node2collection`` map.
Called by the ``DSession.slave_collectionfinish`` hook.
"""
assert node in self.node2pending
if self.collection_is_completed:
# A new node has been added later, perhaps an original one died.
# .schedule() should have
# been called by now
assert self.collection
if collection != self.collection:
other_node = next(iter(self.node2collection.keys()))
msg = report_collection_diff(self.collection,
collection,
other_node.gateway.id,
node.gateway.id)
self.log(msg)
return
self.node2collection[node] = list(collection)
def mark_test_complete(self, node, item_index, duration=0):
"""Mark test item as completed by node
The duration it took to execute the item is used as a hint to
the scheduler.
This is called by the ``DSession.slave_testreport`` hook.
"""
self.node2pending[node].remove(item_index)
self.check_schedule(node, duration=duration)
def check_schedule(self, node, duration=0):
"""Maybe schedule new items on the node
If there are any globally pending nodes left then this will
check if the given node should be given any more tests. The
``duration`` of the last test is optionally used as a
heuristic to influence how many tests the node is assigned.
"""
if node.shutting_down:
return
if self.pending:
# how many nodes do we have?
num_nodes = len(self.node2pending)
# if our node goes below a heuristic minimum, fill it out to
# heuristic maximum
items_per_node_min = max(2, len(self.pending) // num_nodes // 4)
items_per_node_max = max(2, len(self.pending) // num_nodes // 2)
node_pending = self.node2pending[node]
if len(node_pending) < items_per_node_min:
if duration >= 0.1 and len(node_pending) >= 2:
# seems the node is doing long-running tests
# and has enough items to continue
# so let's rather wait with sending new items
return
num_send = items_per_node_max - len(node_pending)
self._send_tests(node, num_send)
self.log("num items waiting for node:", len(self.pending))
def remove_node(self, node):
"""Remove a node from the scheduler
This should be called either when the node crashed or at
shutdown time. In the former case any pending items assigned
to the node will be re-scheduled. Called by the
``DSession.slave_slavefinished`` and
``DSession.slave_errordown`` hooks.
Return the item which was being executing while the node
crashed or None if the node has no more pending items.
"""
pending = self.node2pending.pop(node)
if not pending:
return
# The node crashed, reassing pending items
crashitem = self.collection[pending.pop(0)]
self.pending.extend(pending)
for node in self.node2pending:
self.check_schedule(node)
return crashitem
def schedule(self):
"""Initiate distribution of the test collection
Initiate scheduling of the items across the nodes. If this
gets called again later it behaves the same as calling
``.check_schedule()`` on all nodes so that newly added nodes
will start to be used.
This is called by the ``DSession.slave_collectionfinish`` hook
if ``.collection_is_completed`` is True.
"""
assert self.collection_is_completed
# Initial distribution already happened, reschedule on all nodes
if self.collection is not None:
for node in self.nodes:
self.check_schedule(node)
return
# XXX allow nodes to have different collections
if not self._check_nodes_have_same_collection():
self.log('**Different tests collected, aborting run**')
return
# Collections are identical, create the index of pending items.
self.collection = list(self.node2collection.values())[0]
self.pending[:] = range(len(self.collection))
if not self.collection:
return
# 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
# shutdown signals and get all nodes working.
initial_batch = max(len(self.pending) // 4,
2 * len(self.nodes))
# distribute tests round-robin up to the batch size
# (or until we run out)
nodes = itertools.cycle(self.nodes)
for i in range(initial_batch):
self._send_tests(next(nodes), 1)
if not self.pending:
# initial distribution sent all tests, start node shutdown
for node in self.nodes:
node.shutdown()
def _send_tests(self, node, num):
tests_per_node = self.pending[:num]
if tests_per_node:
del self.pending[:num]
self.node2pending[node].extend(tests_per_node)
node.send_runtest_some(tests_per_node)
def _check_nodes_have_same_collection(self):
"""Return True if all nodes have collected the same items.
If collections differ, this method returns False while logging
the collection differences and posting collection errors to
pytest_collectreport hook.
"""
node_collection_items = list(self.node2collection.items())
first_node, col = node_collection_items[0]
same_collection = True
for node, collection in node_collection_items[1:]:
msg = report_collection_diff(
col,
collection,
first_node.gateway.id,
node.gateway.id,
)
if msg:
same_collection = False
self.log(msg)
if self.config is not None:
rep = CollectReport(
node.gateway.id, 'failed',
longrepr=msg, result=[])
self.config.hook.pytest_collectreport(report=rep)
return same_collection
def report_collection_diff(from_collection, to_collection, from_id, to_id):
"""Report the collected test difference between two nodes.
:returns: detailed message describing the difference between the given
collections, or None if they are equal.
"""
if from_collection == to_collection:
return None
diff = difflib.unified_diff(
from_collection,
to_collection,
fromfile=from_id,
tofile=to_id,
)
error_message = py.builtin._totext(
'Different tests were collected between {from_id} and {to_id}. '
'The difference is:\n'
'{diff}'
).format(from_id=from_id, to_id=to_id, diff='\n'.join(diff))
msg = "\n".join([x.rstrip() for x in error_message.split("\n")])
return msg
class Interrupted(KeyboardInterrupt):
""" signals an immediate interruption. """
class DSession:
"""A py.test plugin which runs a distributed test session
At the beginning of the test session this creates a NodeManager
instance which creates and starts all nodes. Nodes then emit
events processed in the pytest_runtestloop hook using the slave_*
methods.
Once a node is started it will automatically start running the
py.test mainloop with some custom hooks. This means a node
automatically starts collecting tests. Once tests are collected
it will wait for instructions.
"""
def __init__(self, config):
self.config = config
self.log = py.log.Producer("dsession")
if not config.option.debug:
py.log.setconsumer(self.log._keywords, None)
self.nodemanager = None
self.sched = None
self.shuttingdown = False
self.countfailures = 0
self.maxfail = config.getvalue("maxfail")
self.queue = queue.Queue()
self._session = None
self._failed_collection_errors = {}
self._active_nodes = set()
self._failed_nodes_count = 0
self._max_slave_restart = self.config.getoption('max_slave_restart')
if self._max_slave_restart is not None:
self._max_slave_restart = int(self._max_slave_restart)
try:
self.terminal = config.pluginmanager.getplugin("terminalreporter")
except KeyError:
self.terminal = None
else:
self.trdist = TerminalDistReporter(config)
config.pluginmanager.register(self.trdist, "terminaldistreporter")
@property
def session_finished(self):
"""Return True if the distributed session has finished
This means all nodes have executed all test items. This is
used by pytest_runtestloop to break out of its loop.
"""
return bool(self.shuttingdown and not self._active_nodes)
def report_line(self, line):
if self.terminal and self.config.option.verbose >= 0:
self.terminal.write_line(line)
@pytest.mark.trylast
def pytest_sessionstart(self, session):
"""Creates and starts the nodes.
The nodes are setup to put their events onto self.queue. As
soon as nodes start they will emit the slave_slaveready event.
"""
self.nodemanager = NodeManager(self.config)
nodes = self.nodemanager.setup_nodes(putevent=self.queue.put)
self._active_nodes.update(nodes)
self._session = session
def pytest_sessionfinish(self, session):
"""Shutdown all nodes."""
nm = getattr(self, 'nodemanager', None) # if not fully initialized
if nm is not None:
nm.teardown_nodes()
self._session = None
def pytest_collection(self):
# prohibit collection of test items in master process
return True
@pytest.mark.trylast
def pytest_xdist_make_scheduler(self, config, log):
dist = config.getvalue("dist")
if dist == "load":
return LoadScheduling(config, log)
elif dist == "each":
return EachScheduling(config, log)
def pytest_runtestloop(self):
self.sched = self.config.hook.pytest_xdist_make_scheduler(
config=self.config,
log=self.log
)
assert self.sched is not None
self.shouldstop = False
while not self.session_finished:
self.loop_once()
if self.shouldstop:
self.triggershutdown()
raise Interrupted(str(self.shouldstop))
return True
def loop_once(self):
"""Process one callback from one of the slaves."""
while 1:
try:
eventcall = self.queue.get(timeout=2.0)
break
except queue.Empty:
continue
callname, kwargs = eventcall
assert callname, kwargs
method = "slave_" + callname
call = getattr(self, method)
self.log("calling method", method, kwargs)
call(**kwargs)
if self.sched.tests_finished:
self.triggershutdown()
#
# callbacks for processing events from slaves
#
def slave_slaveready(self, node, slaveinfo):
"""Emitted when a node first starts up.
This adds the node to the scheduler, nodes continue with
collection without any further input.
"""
node.slaveinfo = slaveinfo
node.slaveinfo['id'] = node.gateway.id
node.slaveinfo['spec'] = node.gateway.spec
self.config.hook.pytest_testnodeready(node=node)
if self.shuttingdown:
node.shutdown()
else:
self.sched.add_node(node)
def slave_slavefinished(self, node):
"""Emitted when node executes its pytest_sessionfinish hook.
Removes the node from the scheduler.
The node might not be in the scheduler if it had not emitted
slaveready before shutdown was triggered.
"""
self.config.hook.pytest_testnodedown(node=node, error=None)
if node.slaveoutput['exitstatus'] == 2: # keyboard-interrupt
self.shouldstop = "%s received keyboard-interrupt" % (node,)
self.slave_errordown(node, "keyboard-interrupt")
return
if node in self.sched.nodes:
crashitem = self.sched.remove_node(node)
assert not crashitem, (crashitem, node)
self._active_nodes.remove(node)
def slave_errordown(self, node, error):
"""Emitted by the SlaveController when a node dies."""
self.config.hook.pytest_testnodedown(node=node, error=error)
try:
crashitem = self.sched.remove_node(node)
except KeyError:
pass
else:
if crashitem:
self.handle_crashitem(crashitem, node)
self._failed_nodes_count += 1
maximum_reached = (self._max_slave_restart is not None and
self._failed_nodes_count > self._max_slave_restart)
if maximum_reached:
if self._max_slave_restart == 0:
msg = 'Slave restarting disabled'
else:
msg = "Maximum crashed slaves reached: %d" % \
self._max_slave_restart
self.report_line(msg)
else:
self.report_line("Replacing crashed slave %s" % node.gateway.id)
self._clone_node(node)
self._active_nodes.remove(node)
def slave_collectionfinish(self, node, ids):
"""Slave has finished test collection.
This adds the collection for this node to the scheduler. If
the scheduler indicates collection is finished (i.e. all
initial nodes have submitted their collections), then tells the
scheduler to schedule the collected items. When initiating
scheduling the first time it logs which scheduler is in use.
"""
if self.shuttingdown:
return
self.config.hook.pytest_xdist_node_collection_finished(node=node,
ids=ids)
# tell session which items were effectively collected otherwise
# the master node will finish the session with EXIT_NOTESTSCOLLECTED
self._session.testscollected = len(ids)
self.sched.add_node_collection(node, ids)
if self.terminal:
self.trdist.setstatus(node.gateway.spec, "[%d]" % (len(ids)))
if self.sched.collection_is_completed:
if self.terminal and not self.sched.has_pending:
self.trdist.ensure_show_status()
self.terminal.write_line("")
self.terminal.write_line("scheduling tests via %s" % (
self.sched.__class__.__name__))
self.sched.schedule()
def slave_logstart(self, node, nodeid, location):
"""Emitted when a node calls the pytest_runtest_logstart hook."""
self.config.hook.pytest_runtest_logstart(
nodeid=nodeid, location=location)
def slave_testreport(self, node, rep):
"""Emitted when a node calls the pytest_runtest_logreport hook.
If the node indicates it is finished with a test item, remove
the item from the pending list in the scheduler.
"""
if rep.when == "teardown" or (rep.when == "setup" and not rep.passed):
self.sched.mark_test_complete(node, rep.item_index, rep.duration)
# self.report_line("testreport %s: %s" %(rep.id, rep.status))
rep.node = node
self.config.hook.pytest_runtest_logreport(report=rep)
self._handlefailures(rep)
def slave_collectreport(self, node, rep):
"""Emitted when a node calls the pytest_collectreport hook."""
if rep.failed:
self._failed_slave_collectreport(node, rep)
def _clone_node(self, node):
"""Return new node based on an existing one.
This is normally for when a node dies, this will copy the spec
of the existing node and create a new one with a new id. The
new node will have been setup so it will start calling the
"slave_*" hooks and do work soon.
"""
spec = node.gateway.spec
spec.id = None
self.nodemanager.group.allocate_id(spec)
node = self.nodemanager.setup_node(spec, self.queue.put)
self._active_nodes.add(node)
return node
def _failed_slave_collectreport(self, node, rep):
# Check we haven't already seen this report (from
# another slave).
if rep.longrepr not in self._failed_collection_errors:
self._failed_collection_errors[rep.longrepr] = True
self.config.hook.pytest_collectreport(report=rep)
self._handlefailures(rep)
def _handlefailures(self, rep):
if rep.failed:
self.countfailures += 1
if self.maxfail and self.countfailures >= self.maxfail:
self.shouldstop = "stopping after %d failures" % (
self.countfailures)
def triggershutdown(self):
self.log("triggering shutdown")
self.shuttingdown = True
for node in self.sched.nodes:
node.shutdown()
def handle_crashitem(self, nodeid, slave):
# XXX get more reporting info by recording pytest_runtest_logstart?
# XXX count no of failures and retry N times
runner = self.config.pluginmanager.getplugin("runner")
fspath = nodeid.split("::")[0]
msg = "Slave %r crashed while running %r" % (slave.gateway.id, nodeid)
rep = runner.TestReport(nodeid, (fspath, None, fspath),
(), "failed", msg, "???")
rep.node = slave
self.config.hook.pytest_runtest_logreport(report=rep)
class TerminalDistReporter:
def __init__(self, config):
self.config = config
self.tr = config.pluginmanager.getplugin("terminalreporter")
self._status = {}
self._lastlen = 0
self._isatty = getattr(self.tr, 'isatty', self.tr.hasmarkup)
def write_line(self, msg):
self.tr.write_line(msg)
def ensure_show_status(self):
if not self._isatty:
self.write_line(self.getstatus())
def setstatus(self, spec, status, show=True):
self._status[spec.id] = status
if show and self._isatty:
self.rewrite(self.getstatus())
def getstatus(self):
parts = ["%s %s" % (spec.id, self._status[spec.id])
for spec in self._specs]
return " / ".join(parts)
def rewrite(self, line, newline=False):
pline = line + " " * max(self._lastlen-len(line), 0)
if newline:
self._lastlen = 0
pline += "\n"
else:
self._lastlen = len(line)
self.tr.rewrite(pline, bold=True)
def pytest_xdist_setupnodes(self, specs):
self._specs = specs
for spec in specs:
self.setstatus(spec, "I", show=False)
self.setstatus(spec, "I", show=True)
self.ensure_show_status()
def pytest_xdist_newgateway(self, gateway):
if self.config.option.verbose > 0:
rinfo = gateway._rinfo()
version = "%s.%s.%s" % rinfo.version_info[:3]
self.rewrite("[%s] %s Python %s cwd: %s" % (
gateway.id, rinfo.platform, version, rinfo.cwd),
newline=True)
self.setstatus(gateway.spec, "C")
def pytest_testnodeready(self, node):
if self.config.option.verbose > 0:
d = node.slaveinfo
infoline = "[%s] Python %s" % (
d['id'],
d['version'].replace('\n', ' -- '),)
self.rewrite(infoline, newline=True)
self.setstatus(node.gateway.spec, "ok")
def pytest_testnodedown(self, node, error):
if not error:
return
self.write_line("[%s] node down: %s" % (node.gateway.id, error))
# def pytest_xdist_rsyncstart(self, source, gateways):
# targets = ",".join([gw.id for gw in gateways])
# msg = "[%s] rsyncing: %s" %(targets, source)
# self.write_line(msg)
# def pytest_xdist_rsyncfinish(self, source, gateways):
# targets = ", ".join(["[%s]" % gw.id for gw in gateways])
# self.write_line("rsyncfinish: %s -> %s" %(source, targets))

View File

@@ -1,261 +0,0 @@
"""
Implement -f aka looponfailing for py.test.
NOTE that we try to avoid loading and depending on application modules
within the controlling process (the one that starts repeatedly test
processes) otherwise changes to source code can crash
the controlling process which should best never happen.
"""
import py
import pytest
import sys
import execnet
def pytest_addoption(parser):
group = parser.getgroup("xdist", "distributed and subprocess testing")
group._addoption(
'-f', '--looponfail',
action="store_true", dest="looponfail", default=False,
help="run tests in subprocess, wait for modified files "
"and re-run failing test set until all pass.")
def pytest_cmdline_main(config):
if config.getoption("looponfail"):
usepdb = config.getoption('usepdb') # a core option
if usepdb:
raise pytest.UsageError(
"--pdb incompatible with --looponfail.")
looponfail_main(config)
return 2 # looponfail only can get stop with ctrl-C anyway
def looponfail_main(config):
remotecontrol = RemoteControl(config)
rootdirs = config.getini("looponfailroots")
statrecorder = StatRecorder(rootdirs)
try:
while 1:
remotecontrol.loop_once()
if not remotecontrol.failures and remotecontrol.wasfailing:
# the last failures passed, let's immediately rerun all
continue
repr_pytest_looponfailinfo(
failreports=remotecontrol.failures,
rootdirs=rootdirs)
statrecorder.waitonchange(checkinterval=2.0)
except KeyboardInterrupt:
print()
class RemoteControl(object):
def __init__(self, config):
self.config = config
self.failures = []
def trace(self, *args):
if self.config.option.debug:
msg = " ".join([str(x) for x in args])
py.builtin.print_("RemoteControl:", msg)
def initgateway(self):
return execnet.makegateway("popen")
def setup(self, out=None):
if out is None:
out = py.io.TerminalWriter()
if hasattr(self, 'gateway'):
raise ValueError("already have gateway %r" % self.gateway)
self.trace("setting up slave session")
self.gateway = self.initgateway()
self.channel = channel = self.gateway.remote_exec(
init_slave_session,
args=self.config.args,
option_dict=vars(self.config.option),
)
remote_outchannel = channel.receive()
def write(s):
out._file.write(s)
out._file.flush()
remote_outchannel.setcallback(write)
def ensure_teardown(self):
if hasattr(self, 'channel'):
if not self.channel.isclosed():
self.trace("closing", self.channel)
self.channel.close()
del self.channel
if hasattr(self, 'gateway'):
self.trace("exiting", self.gateway)
self.gateway.exit()
del self.gateway
def runsession(self):
try:
self.trace("sending", self.failures)
self.channel.send(self.failures)
try:
return self.channel.receive()
except self.channel.RemoteError:
e = sys.exc_info()[1]
self.trace("ERROR", e)
raise
finally:
self.ensure_teardown()
def loop_once(self):
self.setup()
self.wasfailing = self.failures and len(self.failures)
result = self.runsession()
failures, reports, collection_failed = result
if collection_failed:
pass # "Collection failed, keeping previous failure set"
else:
uniq_failures = []
for failure in failures:
if failure not in uniq_failures:
uniq_failures.append(failure)
self.failures = uniq_failures
def repr_pytest_looponfailinfo(failreports, rootdirs):
tr = py.io.TerminalWriter()
if failreports:
tr.sep("#", "LOOPONFAILING", bold=True)
for report in failreports:
if report:
tr.line(report, red=True)
tr.sep("#", "waiting for changes", bold=True)
for rootdir in rootdirs:
tr.line("### Watching: %s" % (rootdir,), bold=True)
def init_slave_session(channel, args, option_dict):
import os
import sys
outchannel = channel.gateway.newchannel()
sys.stdout = sys.stderr = outchannel.makefile('w')
channel.send(outchannel)
# prune sys.path to not contain relative paths
newpaths = []
for p in sys.path:
if p:
if not os.path.isabs(p):
p = os.path.abspath(p)
newpaths.append(p)
sys.path[:] = newpaths
# fullwidth, hasmarkup = channel.receive()
from _pytest.config import Config
config = Config.fromdictargs(option_dict, list(args))
config.args = args
from xdist.looponfail import SlaveFailSession
SlaveFailSession(config, channel).main()
class SlaveFailSession:
def __init__(self, config, channel):
self.config = config
self.channel = channel
self.recorded_failures = []
self.collection_failed = False
config.pluginmanager.register(self)
config.option.looponfail = False
config.option.usepdb = False
def DEBUG(self, *args):
if self.config.option.debug:
print(" ".join(map(str, args)))
def pytest_collection(self, session):
self.session = session
self.trails = self.current_command
hook = self.session.ihook
try:
items = session.perform_collect(self.trails or None)
except pytest.UsageError:
items = session.perform_collect(None)
hook.pytest_collection_modifyitems(
session=session, config=session.config, items=items)
hook.pytest_collection_finish(session=session)
return True
def pytest_runtest_logreport(self, report):
if report.failed:
self.recorded_failures.append(report)
def pytest_collectreport(self, report):
if report.failed:
self.recorded_failures.append(report)
self.collection_failed = True
def main(self):
self.DEBUG("SLAVE: received configuration, waiting for command trails")
try:
command = self.channel.receive()
except KeyboardInterrupt:
return # in the slave we can't do much about this
self.DEBUG("received", command)
self.current_command = command
self.config.hook.pytest_cmdline_main(config=self.config)
trails, failreports = [], []
for rep in self.recorded_failures:
trails.append(rep.nodeid)
loc = rep.longrepr
loc = str(getattr(loc, 'reprcrash', loc))
failreports.append(loc)
self.channel.send((trails, failreports, self.collection_failed))
class StatRecorder:
def __init__(self, rootdirlist):
self.rootdirlist = rootdirlist
self.statcache = {}
self.check() # snapshot state
def fil(self, p):
return p.check(file=1, dotfile=0) and p.ext != ".pyc"
def rec(self, p):
return p.check(dotfile=0)
def waitonchange(self, checkinterval=1.0):
while 1:
changed = self.check()
if changed:
return
py.std.time.sleep(checkinterval)
def check(self, removepycfiles=True): # noqa, too complex
changed = False
statcache = self.statcache
newstat = {}
for rootdir in self.rootdirlist:
for path in rootdir.visit(self.fil, self.rec):
oldstat = statcache.pop(path, None)
try:
newstat[path] = curstat = path.stat()
except py.error.ENOENT:
if oldstat:
changed = True
else:
if oldstat:
if oldstat.mtime != curstat.mtime or \
oldstat.size != curstat.size:
changed = True
py.builtin.print_("# MODIFIED", path)
if removepycfiles and path.ext == ".py":
pycfile = path + "c"
if pycfile.check():
pycfile.remove()
else:
changed = True
if statcache:
changed = True
self.statcache = newstat
return changed

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,120 +0,0 @@
import py
import pytest
def parse_numprocesses(s):
if s == 'auto':
try:
from os import cpu_count
except ImportError:
from multiprocessing import cpu_count
try:
n = cpu_count()
except NotImplementedError:
return 1
return n if n else 1
else:
return int(s)
def pytest_addoption(parser):
group = parser.getgroup("xdist", "distributed and subprocess testing")
group._addoption(
'-n', dest="numprocesses", metavar="numprocesses",
action="store",
type=parse_numprocesses,
help="shortcut for '--dist=load --tx=NUM*popen', "
"you can use 'auto' here for auto detection CPUs number on "
"host system")
group._addoption('--max-slave-restart', action="store", default=None,
help="maximum number of slaves that can be restarted "
"when crashed (set to zero to disable this feature)")
group._addoption(
'--dist', metavar="distmode",
action="store", choices=['load', 'each', 'no'],
dest="dist", default="no",
help=("set mode for distributing tests to exec environments.\n\n"
"each: send each test to each available environment.\n\n"
"load: send each test to 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.")
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":
from xdist.dsession import DSession
session = DSession(config)
config.pluginmanager.register(session, "dsession")
tr = config.pluginmanager.getplugin("terminalreporter")
tr.showfspath = False
@pytest.mark.tryfirst
def pytest_cmdline_main(config):
if config.option.numprocesses:
config.option.dist = "load"
config.option.tx = ['popen'] * config.option.numprocesses
if config.option.distload:
config.option.dist = "load"
val = config.getvalue
if not val("collectonly"):
usepdb = config.getoption('usepdb') # a core option
if val("dist") != "no":
if usepdb:
raise pytest.UsageError(
"--pdb incompatible with distributing tests.")
# -------------------------------------------------------------------------
# fixtures
# -------------------------------------------------------------------------
@pytest.fixture(scope="session")
def worker_id(request):
if hasattr(request.config, 'slaveinput'):
return request.config.slaveinput['slaveid']
else:
return 'master'

View File

@@ -1,160 +0,0 @@
"""
This module is executed in remote subprocesses and helps to
control a remote testing session and relay back information.
It assumes that 'py' is importable and does not have dependencies
on the rest of the xdist code. This means that the xdist-plugin
needs not to be installed in remote environments.
"""
import sys
import os
class SlaveInteractor:
def __init__(self, config, channel):
self.config = config
self.slaveid = config.slaveinput.get('slaveid', "?")
self.log = py.log.Producer("slave-%s" % self.slaveid)
if not config.option.debug:
py.log.setconsumer(self.log._keywords, None)
self.channel = channel
config.pluginmanager.register(self)
def sendevent(self, name, **kwargs):
self.log("sending", name, kwargs)
self.channel.send((name, kwargs))
def pytest_internalerror(self, excrepr):
for line in str(excrepr).split("\n"):
self.log("IERROR>", line)
def pytest_sessionstart(self, session):
self.session = session
slaveinfo = getinfodict()
self.sendevent("slaveready", slaveinfo=slaveinfo)
def pytest_sessionfinish(self, __multicall__, exitstatus):
self.config.slaveoutput['exitstatus'] = exitstatus
res = __multicall__.execute()
self.sendevent("slavefinished", slaveoutput=self.config.slaveoutput)
return res
def pytest_collection(self, session):
self.sendevent("collectionstart")
def pytest_runtestloop(self, session):
self.log("entering main loop")
torun = []
while 1:
try:
name, kwargs = self.channel.receive()
except EOFError:
return True
self.log("received command", name, kwargs)
if name == "runtests":
torun.extend(kwargs['indices'])
elif name == "runtests_all":
torun.extend(range(len(session.items)))
self.log("items to run:", torun)
# only run if we have an item and a next item
while len(torun) >= 2:
self.run_tests(torun)
if name == "shutdown":
if torun:
self.run_tests(torun)
break
return True
def run_tests(self, torun):
items = self.session.items
self.item_index = torun.pop(0)
if torun:
nextitem = items[torun[0]]
else:
nextitem = None
self.config.hook.pytest_runtest_protocol(
item=items[self.item_index],
nextitem=nextitem)
def pytest_collection_finish(self, session):
self.sendevent(
"collectionfinish",
topdir=str(session.fspath),
ids=[item.nodeid for item in session.items])
def pytest_runtest_logstart(self, nodeid, location):
self.sendevent("logstart", nodeid=nodeid, location=location)
def pytest_runtest_logreport(self, report):
data = serialize_report(report)
data["item_index"] = self.item_index
data["worker_id"] = self.slaveid
assert self.session.items[self.item_index].nodeid == report.nodeid
self.sendevent("testreport", data=data)
def pytest_collectreport(self, report):
data = serialize_report(report)
self.sendevent("collectreport", data=data)
def serialize_report(rep):
import py
d = rep.__dict__.copy()
if hasattr(rep.longrepr, 'toterminal'):
d['longrepr'] = str(rep.longrepr)
else:
d['longrepr'] = rep.longrepr
for name in d:
if isinstance(d[name], py.path.local):
d[name] = str(d[name])
elif name == "result":
d[name] = None # for now
return d
def getinfodict():
import platform
return dict(
version=sys.version,
version_info=tuple(sys.version_info),
sysplatform=sys.platform,
platform=platform.platform(),
executable=sys.executable,
cwd=os.getcwd(),
)
def remote_initconfig(option_dict, args):
from _pytest.config import Config
option_dict['plugins'].append("no:terminal")
config = Config.fromdictargs(option_dict, args)
config.option.looponfail = False
config.option.usepdb = False
config.option.dist = "no"
config.option.distload = False
config.option.numprocesses = None
config.args = args
return config
if __name__ == '__channelexec__':
channel = channel # noqa
# python3.2 is not concurrent import safe, so let's play it safe
# https://bitbucket.org/hpk42/pytest/issue/347/pytest-xdist-and-python-32
if sys.version_info[:2] == (3, 2):
os.environ["PYTHONDONTWRITEBYTECODE"] = "1"
slaveinput, args, option_dict = channel.receive()
importpath = os.getcwd()
sys.path.insert(0, importpath) # XXX only for remote situations
os.environ['PYTHONPATH'] = (
importpath + os.pathsep +
os.environ.get('PYTHONPATH', ''))
os.environ['PYTEST_XDIST_WORKER'] = slaveinput['slaveid']
os.environ['PYTEST_XDIST_WORKER_COUNT'] = str(slaveinput['slavecount'])
# os.environ['PYTHONPATH'] = importpath
import py
config = remote_initconfig(option_dict, args)
config.slaveinput = slaveinput
config.slaveoutput = {}
interactor = SlaveInteractor(config, channel)
config.hook.pytest_cmdline_main(config=config)

View File

@@ -1,333 +0,0 @@
import fnmatch
import os
import re
import py
import pytest
import execnet
import xdist.remote
from _pytest import runner # XXX load dynamically
def parse_spec_config(config):
xspeclist = []
for xspec in config.getvalue("tx"):
i = xspec.find("*")
try:
num = int(xspec[:i])
except ValueError:
xspeclist.append(xspec)
else:
xspeclist.extend([xspec[i + 1:]] * num)
if not xspeclist:
raise pytest.UsageError(
"MISSING test execution (tx) nodes: please specify --tx")
return xspeclist
class NodeManager(object):
EXIT_TIMEOUT = 10
DEFAULT_IGNORES = ['.*', '*.pyc', '*.pyo', '*~']
def __init__(self, config, specs=None, defaultchdir="pyexecnetcache"):
self.config = config
self._nodesready = py.std.threading.Event()
self.trace = self.config.trace.get("nodemanager")
self.group = execnet.Group()
if specs is None:
specs = self._getxspecs()
self.specs = []
for spec in specs:
if not isinstance(spec, execnet.XSpec):
spec = execnet.XSpec(spec)
if not spec.chdir and not spec.popen:
spec.chdir = defaultchdir
self.group.allocate_id(spec)
self.specs.append(spec)
self.roots = self._getrsyncdirs()
self.rsyncoptions = self._getrsyncoptions()
self._rsynced_specs = py.builtin.set()
def rsync_roots(self, gateway):
"""Rsync the set of roots to the node's gateway cwd."""
if self.roots:
for root in self.roots:
self.rsync(gateway, root, **self.rsyncoptions)
def setup_nodes(self, putevent):
self.config.hook.pytest_xdist_setupnodes(config=self.config,
specs=self.specs)
self.trace("setting up nodes")
nodes = []
for spec in self.specs:
nodes.append(self.setup_node(spec, putevent))
return nodes
def setup_node(self, spec, putevent):
gw = self.group.makegateway(spec)
self.config.hook.pytest_xdist_newgateway(gateway=gw)
self.rsync_roots(gw)
node = SlaveController(self, gw, self.config, putevent)
gw.node = node # keep the node alive
node.setup()
self.trace("started node %r" % node)
return node
def teardown_nodes(self):
self.group.terminate(self.EXIT_TIMEOUT)
def _getxspecs(self):
return [execnet.XSpec(x) for x in parse_spec_config(self.config)]
def _getrsyncdirs(self):
for spec in self.specs:
if not spec.popen or spec.chdir:
break
else:
return []
import pytest
import _pytest
pytestpath = pytest.__file__.rstrip("co")
pytestdir = py.path.local(_pytest.__file__).dirpath()
config = self.config
candidates = [py._pydir, pytestpath, pytestdir]
candidates += config.option.rsyncdir
rsyncroots = config.getini("rsyncdirs")
if rsyncroots:
candidates.extend(rsyncroots)
roots = []
for root in candidates:
root = py.path.local(root).realpath()
if not root.check():
raise pytest.UsageError("rsyncdir doesn't exist: %r" % (root,))
if root not in roots:
roots.append(root)
return roots
def _getrsyncoptions(self):
"""Get options to be passed for rsync."""
ignores = list(self.DEFAULT_IGNORES)
ignores += self.config.option.rsyncignore
ignores += self.config.getini("rsyncignore")
return {
'ignores': ignores,
'verbose': self.config.option.verbose,
}
def rsync(self, gateway, source, notify=None, verbose=False, ignores=None):
"""Perform rsync to remote hosts for node."""
# XXX This changes the calling behaviour of
# pytest_xdist_rsyncstart and pytest_xdist_rsyncfinish to
# be called once per rsync target.
rsync = HostRSync(source, verbose=verbose, ignores=ignores)
spec = gateway.spec
if spec.popen and not spec.chdir:
# XXX This assumes that sources are python-packages
# and that adding the basedir does not hurt.
gateway.remote_exec("""
import sys ; sys.path.insert(0, %r)
""" % os.path.dirname(str(source))).waitclose()
return
if (spec, source) in self._rsynced_specs:
return
def finished():
if notify:
notify("rsyncrootready", spec, source)
rsync.add_target_host(gateway, finished=finished)
self._rsynced_specs.add((spec, source))
self.config.hook.pytest_xdist_rsyncstart(
source=source,
gateways=[gateway],
)
rsync.send()
self.config.hook.pytest_xdist_rsyncfinish(
source=source,
gateways=[gateway],
)
class HostRSync(execnet.RSync):
""" RSyncer that filters out common files
"""
def __init__(self, sourcedir, *args, **kwargs):
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):
path = py.path.local(path)
for cre in self._ignores:
if cre.match(path.basename) or cre.match(path.strpath):
return False
else:
return True
def add_target_host(self, gateway, finished=None):
remotepath = os.path.basename(self._sourcedir)
super(HostRSync, self).add_target(gateway, remotepath,
finishedcallback=finished,
delete=True,)
def _report_send_file(self, gateway, modified_rel_path):
if self._verbose:
path = os.path.basename(self._sourcedir) + "/" + modified_rel_path
remotepath = gateway.spec.chdir
py.builtin.print_('%s:%s <= %s' %
(gateway.spec, remotepath, path))
def make_reltoroot(roots, args):
# XXX introduce/use public API for splitting py.test args
splitcode = "::"
l = []
for arg in args:
parts = arg.split(splitcode)
fspath = py.path.local(parts[0])
for root in roots:
x = fspath.relto(root)
if x or fspath == root:
parts[0] = root.basename + "/" + x
break
else:
raise ValueError("arg %s not relative to an rsync root" % (arg,))
l.append(splitcode.join(parts))
return l
class SlaveController(object):
ENDMARK = -1
def __init__(self, nodemanager, gateway, config, putevent):
self.nodemanager = nodemanager
self.putevent = putevent
self.gateway = gateway
self.config = config
self.slaveinput = {'slaveid': gateway.id,
'slavecount': len(nodemanager.specs)}
self._down = False
self._shutdown_sent = False
self.log = py.log.Producer("slavectl-%s" % gateway.id)
if not self.config.option.debug:
py.log.setconsumer(self.log._keywords, None)
def __repr__(self):
return "<%s %s>" % (self.__class__.__name__, self.gateway.id,)
@property
def shutting_down(self):
return self._down or self._shutdown_sent
def setup(self):
self.log("setting up slave session")
spec = self.gateway.spec
args = self.config.args
if not spec.popen or spec.chdir:
args = make_reltoroot(self.nodemanager.roots, args)
option_dict = vars(self.config.option)
if spec.popen:
name = "popen-%s" % self.gateway.id
if hasattr(self.config, '_tmpdirhandler'):
basetemp = self.config._tmpdirhandler.getbasetemp()
option_dict['basetemp'] = str(basetemp.join(name))
self.config.hook.pytest_configure_node(node=self)
self.channel = self.gateway.remote_exec(xdist.remote)
self.channel.send((self.slaveinput, args, option_dict))
if self.putevent:
self.channel.setcallback(
self.process_from_remote,
endmarker=self.ENDMARK)
def ensure_teardown(self):
if hasattr(self, 'channel'):
if not self.channel.isclosed():
self.log("closing", self.channel)
self.channel.close()
# del self.channel
if hasattr(self, 'gateway'):
self.log("exiting", self.gateway)
self.gateway.exit()
# del self.gateway
def send_runtest_some(self, indices):
self.sendcommand("runtests", indices=indices)
def send_runtest_all(self):
self.sendcommand("runtests_all",)
def shutdown(self):
if not self._down:
try:
self.sendcommand("shutdown")
except IOError:
pass
self._shutdown_sent = True
def sendcommand(self, name, **kwargs):
""" send a named parametrized command to the other side. """
self.log("sending command %s(**%s)" % (name, kwargs))
self.channel.send((name, kwargs))
def notify_inproc(self, eventname, **kwargs):
self.log("queuing %s(**%s)" % (eventname, kwargs))
self.putevent((eventname, kwargs))
def process_from_remote(self, eventcall): # noqa too complex
""" this gets called for each object we receive from
the other side and if the channel closes.
Note that channel callbacks run in the receiver
thread of execnet gateways - we need to
avoid raising exceptions or doing heavy work.
"""
try:
if eventcall == self.ENDMARK:
err = self.channel._getremoteerror()
if not self._down:
if not err or isinstance(err, EOFError):
err = "Not properly terminated" # lost connection?
self.notify_inproc("errordown", node=self, error=err)
self._down = True
return
eventname, kwargs = eventcall
if eventname in ("collectionstart",):
self.log("ignoring %s(%s)" % (eventname, kwargs))
elif eventname == "slaveready":
self.notify_inproc(eventname, node=self, **kwargs)
elif eventname == "slavefinished":
self._down = True
self.slaveoutput = kwargs['slaveoutput']
self.notify_inproc("slavefinished", node=self)
elif eventname == "logstart":
self.notify_inproc(eventname, node=self, **kwargs)
elif eventname in (
"testreport", "collectreport", "teardownreport"):
item_index = kwargs.pop("item_index", None)
rep = unserialize_report(eventname, kwargs['data'])
if item_index is not None:
rep.item_index = item_index
self.notify_inproc(eventname, node=self, rep=rep)
elif eventname == "collectionfinish":
self.notify_inproc(eventname, node=self, ids=kwargs['ids'])
else:
raise ValueError("unknown event: %s" % (eventname,))
except KeyboardInterrupt:
# should not land in receiver-thread
raise
except:
excinfo = py.code.ExceptionInfo()
py.builtin.print_("!" * 20, excinfo)
self.config.pluginmanager.notify_exception(excinfo)
def unserialize_report(name, reportdict):
if name == "testreport":
return runner.TestReport(**reportdict)
elif name == "collectreport":
return runner.CollectReport(**reportdict)