Compare commits
47 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
95b309e980 | ||
|
|
3fe877bc6d | ||
|
|
f36ea25cb5 | ||
|
|
d81f57508d | ||
|
|
1d2616418c | ||
|
|
5f43575b42 | ||
|
|
a6b56112f9 | ||
|
|
64f9388f41 | ||
|
|
230ba6ad10 | ||
|
|
93ca202b40 | ||
|
|
8f3d1ad92f | ||
|
|
ab3d6a9ee5 | ||
|
|
a5210d4103 | ||
|
|
0b6b9c3c38 | ||
|
|
838434c269 | ||
|
|
b85a4717f9 | ||
|
|
b2770e65e1 | ||
|
|
4a3a8b499b | ||
|
|
4016c2a173 | ||
|
|
ddc28f3870 | ||
|
|
02ddfc9860 | ||
|
|
f15de24e5a | ||
|
|
49a1093574 | ||
|
|
470f4ee53c | ||
|
|
733d769bc0 | ||
|
|
8ff5c62cfe | ||
|
|
6b52e0a23d | ||
|
|
d5313c5660 | ||
|
|
bfd28ff398 | ||
|
|
17da7cbe59 | ||
|
|
caff114004 | ||
|
|
6f0cb26fc0 | ||
|
|
5661f498c7 | ||
|
|
380f978243 | ||
|
|
f609fb7de4 | ||
|
|
03a64d91f3 | ||
|
|
7d14d50f65 | ||
|
|
56a4f212b7 | ||
|
|
a4c887b058 | ||
|
|
7e1768f838 | ||
|
|
9791021f02 | ||
|
|
4e7bd0239e | ||
|
|
52a61434fb | ||
|
|
9efc64eef9 | ||
|
|
6d39025be3 | ||
|
|
d8fb60ba7f | ||
|
|
4d230b6de7 |
8
.github/workflows/deploy.yml
vendored
8
.github/workflows/deploy.yml
vendored
@@ -43,5 +43,13 @@ jobs:
|
||||
|
||||
- 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@v1
|
||||
with:
|
||||
files: dist/*
|
||||
tag_name: v${{ github.event.inputs.version }}
|
||||
|
||||
22
.github/workflows/test.yml
vendored
22
.github/workflows/test.yml
vendored
@@ -17,7 +17,7 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
|
||||
check-package:
|
||||
package:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
@@ -26,6 +26,8 @@ jobs:
|
||||
|
||||
test:
|
||||
|
||||
needs: [package]
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
strategy:
|
||||
@@ -36,9 +38,9 @@ jobs:
|
||||
- "py38-pytestlatest"
|
||||
- "py39-pytestlatest"
|
||||
- "py310-pytestlatest"
|
||||
- "py310-pytestmain"
|
||||
- "py311-pytestlatest"
|
||||
- "py311-pytestmain"
|
||||
- "py312-pytestlatest"
|
||||
- "py310-psutil"
|
||||
- "py310-setproctitle"
|
||||
|
||||
@@ -52,12 +54,12 @@ jobs:
|
||||
python: "3.9"
|
||||
- tox_env: "py310-pytestlatest"
|
||||
python: "3.10"
|
||||
- tox_env: "py310-pytestmain"
|
||||
python: "3.10"
|
||||
- tox_env: "py311-pytestlatest"
|
||||
python: "3.11"
|
||||
- tox_env: "py311-pytestmain"
|
||||
python: "3.11"
|
||||
- tox_env: "py312-pytestlatest"
|
||||
python: "3.12"
|
||||
- tox_env: "py310-psutil"
|
||||
python: "3.10"
|
||||
- tox_env: "py310-setproctitle"
|
||||
@@ -68,14 +70,24 @@ jobs:
|
||||
with:
|
||||
# Needed to fetch tags, which are required by setuptools-scm.
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Download Package
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: Packages
|
||||
path: dist
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: ${{ matrix.python }}
|
||||
|
||||
- name: Install tox
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install tox
|
||||
|
||||
- name: Test
|
||||
shell: bash
|
||||
run: |
|
||||
tox -e ${{ matrix.tox_env }}
|
||||
tox run -e ${{ matrix.tox_env }} --installpkg `find dist/*.tar.gz`
|
||||
|
||||
@@ -1,32 +1,32 @@
|
||||
repos:
|
||||
- repo: https://github.com/PyCQA/autoflake
|
||||
rev: v2.1.1
|
||||
rev: v2.2.1
|
||||
hooks:
|
||||
- id: autoflake
|
||||
args: ["--in-place", "--remove-unused-variables", "--remove-all-unused-imports"]
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 23.3.0
|
||||
rev: 23.11.0
|
||||
hooks:
|
||||
- id: black
|
||||
args: [--safe, --quiet, --target-version, py35]
|
||||
- repo: https://github.com/asottile/blacken-docs
|
||||
rev: 1.13.0
|
||||
rev: 1.16.0
|
||||
hooks:
|
||||
- id: blacken-docs
|
||||
additional_dependencies: [black==23.1.0]
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.4.0
|
||||
rev: v4.5.0
|
||||
hooks:
|
||||
- id: trailing-whitespace
|
||||
- id: end-of-file-fixer
|
||||
- id: check-yaml
|
||||
- id: debug-statements
|
||||
- repo: https://github.com/PyCQA/flake8
|
||||
rev: 6.0.0
|
||||
rev: 6.1.0
|
||||
hooks:
|
||||
- id: flake8
|
||||
- repo: https://github.com/asottile/pyupgrade
|
||||
rev: v3.4.0
|
||||
rev: v3.15.0
|
||||
hooks:
|
||||
- id: pyupgrade
|
||||
args: [--py3-plus]
|
||||
@@ -39,7 +39,7 @@ repos:
|
||||
language: python
|
||||
additional_dependencies: [pygments, restructuredtext_lint]
|
||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||
rev: v1.2.0
|
||||
rev: v1.7.0
|
||||
hooks:
|
||||
- id: mypy
|
||||
files: ^(src/|testing/)
|
||||
|
||||
14
.readthedocs.yaml
Normal file
14
.readthedocs.yaml
Normal 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
|
||||
@@ -1,3 +1,40 @@
|
||||
pytest-xdist 3.5.0 (2023-11-21)
|
||||
===============================
|
||||
|
||||
Features
|
||||
--------
|
||||
|
||||
- `#632 <https://github.com/pytest-dev/pytest-xdist/issues/632>`_: ``--dist=loadscope`` now sorts scopes by number of tests to assign largest scopes early -- in many cases this should improve overall test session running time, as there is less chance of a large scope being left to be processed near the end of the session, leaving other workers idle.
|
||||
|
||||
|
||||
pytest-xdist 3.4.0 (2023-11-11)
|
||||
===============================
|
||||
|
||||
Features
|
||||
--------
|
||||
|
||||
- `#963 <https://github.com/pytest-dev/pytest-xdist/issues/963>`_: Wait for workers to finish reporting when test run stops early.
|
||||
|
||||
This makes sure that the results of in-progress tests are displayed.
|
||||
Previously these reports were being discarded, losing information about the
|
||||
test run.
|
||||
|
||||
- `#965 <https://github.com/pytest-dev/pytest-xdist/issues/965>`_: Added support for Python 3.12.
|
||||
|
||||
|
||||
pytest-xdist 3.3.1 (2023-05-19)
|
||||
===============================
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- `#907 <https://github.com/pytest-dev/pytest-xdist/issues/907>`_: Avoid remote calls during startup as ``execnet`` by default does not ensure remote affinity with the
|
||||
main thread and might accidentally schedule the pytest worker into a non-main thread, which breaks numerous frameworks,
|
||||
for example ``asyncio``, ``anyio``, ``PyQt/PySide``, etc.
|
||||
|
||||
A more safe correction will require thread affinity in ``execnet`` (`pytest-dev/execnet#96 <https://github.com/pytest-dev/execnet/issues/96>`__).
|
||||
|
||||
|
||||
pytest-xdist 3.3.0 (2023-05-12)
|
||||
===============================
|
||||
|
||||
|
||||
@@ -29,8 +29,8 @@ The execution flow is:
|
||||
corresponds to the position of that test in the original collection
|
||||
list. This works because all nodes have the same collection list, and
|
||||
saves bandwidth because the **controller** can now tell one of the
|
||||
workers to just *execute test index 3* index of passing the full test
|
||||
id.
|
||||
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.
|
||||
|
||||
@@ -9,13 +9,16 @@ Sending tests to remote SSH accounts
|
||||
|
||||
.. warning::
|
||||
|
||||
This feature is deprecated because the support for ``rsync`` is faulty
|
||||
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.
|
||||
|
||||
This feature is scheduled to be removed in release 4.0, to let the team
|
||||
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
|
||||
|
||||
2
docs/requirements.txt
Normal file
2
docs/requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
sphinx
|
||||
sphinx-rtd-theme
|
||||
@@ -30,6 +30,7 @@ classifiers =
|
||||
Programming Language :: Python :: 3.9
|
||||
Programming Language :: Python :: 3.10
|
||||
Programming Language :: Python :: 3.11
|
||||
Programming Language :: Python :: 3.12
|
||||
license_file = LICENSE
|
||||
project_urls =
|
||||
Documentation=https://pytest-xdist.readthedocs.io/en/latest
|
||||
|
||||
@@ -118,11 +118,14 @@ class DSession:
|
||||
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()
|
||||
raise Interrupted(str(self.shouldstop))
|
||||
pending_exception = Interrupted(str(self.shouldstop))
|
||||
if pending_exception:
|
||||
raise pending_exception
|
||||
return True
|
||||
|
||||
def loop_once(self):
|
||||
@@ -351,14 +354,19 @@ class DSession:
|
||||
def _handlefailures(self, rep):
|
||||
if rep.failed:
|
||||
self.countfailures += 1
|
||||
if self.maxfail and self.countfailures >= self.maxfail:
|
||||
if (
|
||||
self.maxfail
|
||||
and self.countfailures >= self.maxfail
|
||||
and not self.shouldstop
|
||||
):
|
||||
self.shouldstop = f"stopping after {self.countfailures} failures"
|
||||
|
||||
def triggershutdown(self):
|
||||
self.log("triggering shutdown")
|
||||
self.shuttingdown = True
|
||||
for node in self.sched.nodes:
|
||||
node.shutdown()
|
||||
if not self.shuttingdown:
|
||||
self.log("triggering shutdown")
|
||||
self.shuttingdown = True
|
||||
for node in self.sched.nodes:
|
||||
node.shutdown()
|
||||
|
||||
def handle_crashitem(self, nodeid, worker):
|
||||
# XXX get more reporting info by recording pytest_runtest_logstart?
|
||||
@@ -444,26 +452,25 @@ class TerminalDistReporter:
|
||||
|
||||
@pytest.hookimpl
|
||||
def pytest_xdist_newgateway(self, gateway) -> None:
|
||||
rinfo = gateway._rinfo()
|
||||
is_local = rinfo.executable == sys.executable
|
||||
if self.config.option.verbose > 0 and not is_local:
|
||||
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,
|
||||
)
|
||||
if self.config.option.verbose > 0:
|
||||
rinfo = gateway._rinfo()
|
||||
different_interpreter = rinfo.executable != sys.executable
|
||||
if different_interpreter:
|
||||
version = "%s.%s.%s" % 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) -> None:
|
||||
d = node.workerinfo
|
||||
is_local = d.get("executable") == sys.executable
|
||||
if self.config.option.verbose > 0 and not is_local:
|
||||
infoline = "[{}] Python {}".format(
|
||||
d["id"], d["version"].replace("\n", " -- ")
|
||||
)
|
||||
self.rewrite(infoline, newline=True)
|
||||
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
|
||||
)
|
||||
|
||||
@@ -25,14 +25,18 @@ def pytest_xdist_newgateway(gateway):
|
||||
|
||||
|
||||
@pytest.hookspec(
|
||||
warn_on_impl="rsync feature is deprecated and will be removed in pytest-xdist 4.0"
|
||||
warn_on_impl=DeprecationWarning(
|
||||
"rsync feature is deprecated and will be removed in pytest-xdist 4.0"
|
||||
)
|
||||
)
|
||||
def pytest_xdist_rsyncstart(source, gateways):
|
||||
"""called before rsyncing a directory to remote gateways takes place."""
|
||||
|
||||
|
||||
@pytest.hookspec(
|
||||
warn_on_impl="rsync feature is deprecated and will be removed in pytest-xdist 4.0"
|
||||
warn_on_impl=DeprecationWarning(
|
||||
"rsync feature is deprecated and will be removed in pytest-xdist 4.0"
|
||||
)
|
||||
)
|
||||
def pytest_xdist_rsyncfinish(source, gateways):
|
||||
"""called after rsyncing a directory to remote gateways takes place."""
|
||||
|
||||
@@ -350,11 +350,18 @@ class LoadScopeScheduling:
|
||||
return
|
||||
|
||||
# Determine chunks of work (scopes)
|
||||
unsorted_workqueue = OrderedDict()
|
||||
for nodeid in self.collection:
|
||||
scope = self._split_scope(nodeid)
|
||||
work_unit = self.workqueue.setdefault(scope, default=OrderedDict())
|
||||
work_unit = unsorted_workqueue.setdefault(scope, default=OrderedDict())
|
||||
work_unit[nodeid] = False
|
||||
|
||||
# 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
|
||||
|
||||
# Avoid having more workers than work
|
||||
extra_nodes = len(self.nodes) - len(self.workqueue)
|
||||
|
||||
|
||||
@@ -109,18 +109,44 @@ class TestDistribution:
|
||||
)
|
||||
assert result.ret == 1
|
||||
|
||||
def test_n1_fail_minus_x(self, pytester: pytest.Pytester) -> None:
|
||||
def test_exitfail_waits_for_workers_to_finish(
|
||||
self, pytester: pytest.Pytester
|
||||
) -> None:
|
||||
"""The DSession waits for workers before exiting early on failure.
|
||||
|
||||
When -x/--exitfail is set, the DSession wait for the workers to finish
|
||||
before raising an Interrupt exception. This prevents reports from the
|
||||
faiing test and other tests from being discarded.
|
||||
"""
|
||||
p1 = pytester.makepyfile(
|
||||
"""
|
||||
import time
|
||||
|
||||
def test_fail1():
|
||||
time.sleep(0.1)
|
||||
assert 0
|
||||
def test_fail2():
|
||||
time.sleep(0.2)
|
||||
def test_fail3():
|
||||
time.sleep(0.3)
|
||||
assert 0
|
||||
def test_fail4():
|
||||
time.sleep(0.3)
|
||||
def test_fail5():
|
||||
time.sleep(0.3)
|
||||
def test_fail6():
|
||||
time.sleep(0.3)
|
||||
"""
|
||||
)
|
||||
result = pytester.runpytest(p1, "-x", "-v", "-n1")
|
||||
result = pytester.runpytest(p1, "-x", "-rA", "-v", "-n2")
|
||||
assert result.ret == 2
|
||||
result.stdout.fnmatch_lines(["*Interrupted: stopping*1*", "*1 failed*"])
|
||||
result.stdout.re_match_lines([".*Interrupted: stopping.*[12].*"])
|
||||
m = re.search(r"== (\d+) failed, (\d+) passed in ", str(result.stdout))
|
||||
assert m
|
||||
n_failed, n_passed = (int(s) for s in m.groups())
|
||||
assert 1 <= n_failed <= 2
|
||||
assert 1 <= n_passed <= 3
|
||||
assert (n_passed + n_failed) < 6
|
||||
|
||||
def test_basetemp_in_subprocesses(self, pytester: pytest.Pytester) -> None:
|
||||
p1 = pytester.makepyfile(
|
||||
@@ -1150,7 +1176,7 @@ def test_internal_error_with_maxfail(pytester: pytest.Pytester) -> None:
|
||||
"""
|
||||
)
|
||||
result = pytester.runpytest_subprocess("--maxfail=1", "-n1")
|
||||
result.stdout.fnmatch_lines(["* 1 error in *"])
|
||||
result.stdout.re_match_lines([".* [12] errors? in .*"])
|
||||
assert "INTERNALERROR" not in result.stderr.str()
|
||||
|
||||
|
||||
@@ -1206,6 +1232,22 @@ class TestLoadScope:
|
||||
"test_a.py::TestB", result.outlines
|
||||
) in ({"gw0": 10}, {"gw1": 10})
|
||||
|
||||
def test_workqueue_ordered_by_size(self, pytester: pytest.Pytester) -> None:
|
||||
test_file = """
|
||||
import pytest
|
||||
@pytest.mark.parametrize('i', range({}))
|
||||
def test(i):
|
||||
pass
|
||||
"""
|
||||
pytester.makepyfile(test_a=test_file.format(10), test_b=test_file.format(20))
|
||||
result = pytester.runpytest("-n2", "--dist=loadscope", "-v")
|
||||
assert get_workers_and_test_count_by_prefix(
|
||||
"test_a.py::test", result.outlines
|
||||
) == {"gw1": 10}
|
||||
assert get_workers_and_test_count_by_prefix(
|
||||
"test_b.py::test", result.outlines
|
||||
) == {"gw0": 20}
|
||||
|
||||
def test_module_single_start(self, pytester: pytest.Pytester) -> None:
|
||||
"""Fix test suite never finishing in case all workers start with a single test (#277)."""
|
||||
test_file1 = """
|
||||
|
||||
@@ -374,7 +374,7 @@ def test_unserialize_warning_msg(w_cls):
|
||||
v1 = getattr(w_msg, k)
|
||||
v2 = getattr(w_msg2, k)
|
||||
if k == "message":
|
||||
assert type(v1) == type(v2)
|
||||
assert type(v1) is type(v2)
|
||||
assert v1.args == v2.args
|
||||
else:
|
||||
assert v1 == v2
|
||||
|
||||
Reference in New Issue
Block a user