Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
23461ed23b
|
|||
|
|
2b4372bd62 | ||
|
|
1bb1a0f62a | ||
|
|
f8ea4a23b7 | ||
|
|
68bf76dce4 | ||
|
|
28c0a72e3a | ||
|
|
9d7dcae9d2 | ||
|
|
a3374b8d5c | ||
|
|
5824f221ca | ||
|
|
6ca0790e02 | ||
|
|
b59e010821 | ||
|
|
239def9be9 | ||
|
|
ec87d0a139 | ||
|
|
fe10f1c411 | ||
|
|
3572d0b13c | ||
|
|
1e3e4dc165 | ||
|
|
600aad575a | ||
|
|
9d7ba5b5fb | ||
|
|
532f07fb18 | ||
|
|
0883ad085e | ||
|
|
58a51bc140 | ||
|
|
59a2ad0150 | ||
|
|
d42b9c7204 | ||
|
|
ebfcb99072 | ||
|
|
23b7fd6054 |
2
.github/workflows/deploy.yml
vendored
2
.github/workflows/deploy.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
|||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Build and Check Package
|
- name: Build and Check Package
|
||||||
uses: hynek/build-and-inspect-python-package@v2.12
|
uses: hynek/build-and-inspect-python-package@v2.13
|
||||||
|
|
||||||
deploy:
|
deploy:
|
||||||
needs: package
|
needs: package
|
||||||
|
|||||||
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Build and Check Package
|
- name: Build and Check Package
|
||||||
uses: hynek/build-and-inspect-python-package@v2.12
|
uses: hynek/build-and-inspect-python-package@v2.13
|
||||||
|
|
||||||
test:
|
test:
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
rev: "v0.11.10"
|
rev: "v0.12.3"
|
||||||
hooks:
|
hooks:
|
||||||
- id: ruff
|
- id: ruff
|
||||||
args: ["--fix"]
|
args: ["--fix"]
|
||||||
@@ -23,7 +23,7 @@ repos:
|
|||||||
language: python
|
language: python
|
||||||
additional_dependencies: [pygments, restructuredtext_lint]
|
additional_dependencies: [pygments, restructuredtext_lint]
|
||||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||||
rev: v1.15.0
|
rev: v1.16.1
|
||||||
hooks:
|
hooks:
|
||||||
- id: mypy
|
- id: mypy
|
||||||
files: ^(src/|testing/)
|
files: ^(src/|testing/)
|
||||||
|
|||||||
@@ -1,3 +1,15 @@
|
|||||||
|
pytest-xdist 3.8.0 (2025-06-30)
|
||||||
|
===============================
|
||||||
|
|
||||||
|
Features
|
||||||
|
--------
|
||||||
|
|
||||||
|
- `#1083 <https://github.com/pytest-dev/pytest-xdist/issues/1083>`_: Add ``--no-loadscope-reorder`` and ``--loadscope-reorder`` option to control whether to automatically reorder tests in loadscope for tests where relative ordering matters. This only applies when using ``loadscope``.
|
||||||
|
|
||||||
|
For example, [test_file_1, test_file_2, ..., test_file_n] are given as input test files, if ``--no-loadscope-reorder`` is used, for either worker, the ``test_file_a`` will be executed before ``test_file_b`` only if ``a < b``.
|
||||||
|
|
||||||
|
The default behavior is to reorder the tests to maximize the number of tests that can be executed in parallel.
|
||||||
|
|
||||||
pytest-xdist 3.7.0 (2025-05-26)
|
pytest-xdist 3.7.0 (2025-05-26)
|
||||||
===============================
|
===============================
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
[build-system]
|
[build-system]
|
||||||
requires = [
|
requires = [
|
||||||
"setuptools>=77.0",
|
"setuptools>=77.0",
|
||||||
"setuptools-scm[toml]>=6.2.3",
|
|
||||||
]
|
]
|
||||||
build-backend = "setuptools.build_meta"
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "pytest-xdist"
|
name = "pytest-xdist"
|
||||||
|
version = "3.7.1.dev24+g2b4372bd6"
|
||||||
description = "pytest xdist plugin for distributed testing, most importantly across multiple CPUs"
|
description = "pytest xdist plugin for distributed testing, most importantly across multiple CPUs"
|
||||||
readme = "README.rst"
|
readme = "README.rst"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
@@ -35,7 +35,6 @@ dependencies = [
|
|||||||
"execnet>=2.1",
|
"execnet>=2.1",
|
||||||
"pytest>=7.0.0",
|
"pytest>=7.0.0",
|
||||||
]
|
]
|
||||||
dynamic = ["version"]
|
|
||||||
|
|
||||||
[project.urls]
|
[project.urls]
|
||||||
Homepage = "https://github.com/pytest-dev/pytest-xdist"
|
Homepage = "https://github.com/pytest-dev/pytest-xdist"
|
||||||
@@ -53,9 +52,6 @@ testing = ["filelock"]
|
|||||||
psutil = ["psutil>=3.0"]
|
psutil = ["psutil>=3.0"]
|
||||||
setproctitle = ["setproctitle"]
|
setproctitle = ["setproctitle"]
|
||||||
|
|
||||||
[tool.setuptools_scm]
|
|
||||||
write_to = "src/xdist/_version.py"
|
|
||||||
|
|
||||||
[tool.pytest.ini_options]
|
[tool.pytest.ini_options]
|
||||||
# pytest-services also defines a worker_id fixture, disable
|
# pytest-services also defines a worker_id fixture, disable
|
||||||
# it so they don't conflict with each other (#611).
|
# it so they don't conflict with each other (#611).
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
from xdist._version import version as __version__
|
|
||||||
from xdist.plugin import get_xdist_worker_id
|
from xdist.plugin import get_xdist_worker_id
|
||||||
from xdist.plugin import is_xdist_controller
|
from xdist.plugin import is_xdist_controller
|
||||||
from xdist.plugin import is_xdist_master
|
from xdist.plugin import is_xdist_master
|
||||||
from xdist.plugin import is_xdist_worker
|
from xdist.plugin import is_xdist_worker
|
||||||
|
|
||||||
|
|
||||||
|
__version__ = "3.7.1.dev24+g2b4372bd6"
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"__version__",
|
"__version__",
|
||||||
"get_xdist_worker_id",
|
"get_xdist_worker_id",
|
||||||
|
|||||||
@@ -127,6 +127,32 @@ def pytest_addoption(parser: pytest.Parser) -> None:
|
|||||||
"(default) no: Run tests inprocess, don't distribute."
|
"(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(
|
group.addoption(
|
||||||
"--tx",
|
"--tx",
|
||||||
dest="tx",
|
dest="tx",
|
||||||
|
|||||||
@@ -248,7 +248,7 @@ class WorkerInteractor:
|
|||||||
if len(mark.args) > 0
|
if len(mark.args) > 0
|
||||||
else mark.kwargs.get("name", "default")
|
else mark.kwargs.get("name", "default")
|
||||||
)
|
)
|
||||||
gnames.add(name)
|
gnames.add(str(name))
|
||||||
if not gnames:
|
if not gnames:
|
||||||
continue
|
continue
|
||||||
item._nodeid = f"{item.nodeid}@{'_'.join(sorted(gnames))}"
|
item._nodeid = f"{item.nodeid}@{'_'.join(sorted(gnames))}"
|
||||||
|
|||||||
@@ -371,11 +371,15 @@ class LoadScopeScheduling:
|
|||||||
work_unit = unsorted_workqueue.setdefault(scope, {})
|
work_unit = unsorted_workqueue.setdefault(scope, {})
|
||||||
work_unit[nodeid] = False
|
work_unit[nodeid] = False
|
||||||
|
|
||||||
# Insert tests scopes into work queue ordered by number of tests.
|
if self.config.option.loadscopereorder:
|
||||||
for scope, nodeids in sorted(
|
# Insert tests scopes into work queue ordered by number of tests.
|
||||||
unsorted_workqueue.items(), key=lambda item: -len(item[1])
|
for scope, nodeids in sorted(
|
||||||
):
|
unsorted_workqueue.items(), key=lambda item: -len(item[1])
|
||||||
self.workqueue[scope] = nodeids
|
):
|
||||||
|
self.workqueue[scope] = nodeids
|
||||||
|
else:
|
||||||
|
for scope, nodeids in unsorted_workqueue.items():
|
||||||
|
self.workqueue[scope] = nodeids
|
||||||
|
|
||||||
# Avoid having more workers than work
|
# Avoid having more workers than work
|
||||||
extra_nodes = len(self.nodes) - len(self.workqueue)
|
extra_nodes = len(self.nodes) - len(self.workqueue)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections.abc import Sequence
|
from collections.abc import Sequence
|
||||||
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
import enum
|
import enum
|
||||||
import fnmatch
|
import fnmatch
|
||||||
import os
|
import os
|
||||||
@@ -94,15 +95,23 @@ class NodeManager:
|
|||||||
) -> list[WorkerController]:
|
) -> list[WorkerController]:
|
||||||
self.config.hook.pytest_xdist_setupnodes(config=self.config, specs=self.specs)
|
self.config.hook.pytest_xdist_setupnodes(config=self.config, specs=self.specs)
|
||||||
self.trace("setting up nodes")
|
self.trace("setting up nodes")
|
||||||
return [self.setup_node(spec, putevent) for spec in self.specs]
|
with ThreadPoolExecutor(max_workers=len(self.specs)) as executor:
|
||||||
|
futs = [
|
||||||
|
executor.submit(self.setup_node, spec, putevent, idx)
|
||||||
|
for idx, spec in enumerate(self.specs)
|
||||||
|
]
|
||||||
|
return [f.result() for f in futs]
|
||||||
|
|
||||||
def setup_node(
|
def setup_node(
|
||||||
self,
|
self,
|
||||||
spec: execnet.XSpec,
|
spec: execnet.XSpec,
|
||||||
putevent: Callable[[tuple[str, dict[str, Any]]], None],
|
putevent: Callable[[tuple[str, dict[str, Any]]], None],
|
||||||
|
idx: int | None = None,
|
||||||
) -> WorkerController:
|
) -> WorkerController:
|
||||||
if getattr(spec, "execmodel", None) != "main_thread_only":
|
if getattr(spec, "execmodel", None) != "main_thread_only":
|
||||||
spec = execnet.XSpec(f"execmodel=main_thread_only//{spec}")
|
spec = execnet.XSpec(f"execmodel=main_thread_only//{spec}")
|
||||||
|
if idx is not None:
|
||||||
|
spec = execnet.XSpec(f"{spec}//id=gw{idx}")
|
||||||
gw = self.group.makegateway(spec)
|
gw = self.group.makegateway(spec)
|
||||||
self.config.hook.pytest_xdist_newgateway(gateway=gw)
|
self.config.hook.pytest_xdist_newgateway(gateway=gw)
|
||||||
self.rsync_roots(gw)
|
self.rsync_roots(gw)
|
||||||
|
|||||||
@@ -1254,6 +1254,24 @@ class TestLoadScope:
|
|||||||
"test_b.py::test", result.outlines
|
"test_b.py::test", result.outlines
|
||||||
) == {"gw0": 20}
|
) == {"gw0": 20}
|
||||||
|
|
||||||
|
def test_workqueue_ordered_by_input(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", "--no-loadscope-reorder", "-v"
|
||||||
|
)
|
||||||
|
assert get_workers_and_test_count_by_prefix(
|
||||||
|
"test_a.py::test", result.outlines
|
||||||
|
) == {"gw0": 10}
|
||||||
|
assert get_workers_and_test_count_by_prefix(
|
||||||
|
"test_b.py::test", result.outlines
|
||||||
|
) == {"gw1": 20}
|
||||||
|
|
||||||
def test_module_single_start(self, pytester: pytest.Pytester) -> None:
|
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)."""
|
"""Fix test suite never finishing in case all workers start with a single test (#277)."""
|
||||||
test_file1 = """
|
test_file1 = """
|
||||||
|
|||||||
@@ -82,11 +82,19 @@ class TestNodeManagerPopen:
|
|||||||
call = hookrecorder.popcall("pytest_xdist_setupnodes")
|
call = hookrecorder.popcall("pytest_xdist_setupnodes")
|
||||||
assert len(call.specs) == 2
|
assert len(call.specs) == 2
|
||||||
|
|
||||||
call = hookrecorder.popcall("pytest_xdist_newgateway")
|
# check expected gateways
|
||||||
assert call.gateway.spec == execnet.XSpec("execmodel=main_thread_only//popen")
|
gw_calls = [
|
||||||
assert call.gateway.id == "gw0"
|
hookrecorder.popcall("pytest_xdist_newgateway"),
|
||||||
call = hookrecorder.popcall("pytest_xdist_newgateway")
|
hookrecorder.popcall("pytest_xdist_newgateway"),
|
||||||
assert call.gateway.id == "gw1"
|
]
|
||||||
|
assert {c.gateway.id for c in gw_calls} == {"gw0", "gw1"}
|
||||||
|
|
||||||
|
for c in gw_calls:
|
||||||
|
expected_spec = execnet.XSpec(
|
||||||
|
f"execmodel=main_thread_only//popen//id={c.gateway.id}"
|
||||||
|
)
|
||||||
|
assert c.gateway.spec == expected_spec
|
||||||
|
|
||||||
assert len(hm.group) == 2
|
assert len(hm.group) == 2
|
||||||
hm.teardown_nodes()
|
hm.teardown_nodes()
|
||||||
assert not len(hm.group)
|
assert not len(hm.group)
|
||||||
@@ -206,8 +214,8 @@ class TestNodeManager:
|
|||||||
p = Path(p)
|
p = Path(p)
|
||||||
print("remote curdir", p)
|
print("remote curdir", p)
|
||||||
assert p == dest.joinpath(config.rootpath.name)
|
assert p == dest.joinpath(config.rootpath.name)
|
||||||
assert p.joinpath("dir1").check()
|
assert p.joinpath("dir1").is_dir()
|
||||||
assert p.joinpath("dir1", "file1").check()
|
assert p.joinpath("dir1", "file1").is_file()
|
||||||
|
|
||||||
def test_popen_rsync_subdir(
|
def test_popen_rsync_subdir(
|
||||||
self,
|
self,
|
||||||
|
|||||||
Reference in New Issue
Block a user