diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 9363a137..4f33e076 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -19,7 +19,7 @@ jobs: - uses: actions/checkout@v4 - name: Build and Check Package - uses: hynek/build-and-inspect-python-package@v2.12 + uses: hynek/build-and-inspect-python-package@v2.13 deploy: needs: package diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4be6f7de..db3840fd 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -22,7 +22,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Build and Check Package - uses: hynek/build-and-inspect-python-package@v2.12 + uses: hynek/build-and-inspect-python-package@v2.13 test: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2bf5b29c..9fb4a64d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: "v0.11.10" + rev: "v0.12.0" hooks: - id: ruff args: ["--fix"] @@ -23,7 +23,7 @@ repos: language: python additional_dependencies: [pygments, restructuredtext_lint] - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.15.0 + rev: v1.16.1 hooks: - id: mypy files: ^(src/|testing/) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index c3b0f61b..fe5038a1 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,3 +1,15 @@ +pytest-xdist 3.8.0 (2025-06-30) +=============================== + +Features +-------- + +- `#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) =============================== diff --git a/src/xdist/plugin.py b/src/xdist/plugin.py index 0cf90f86..0a07b712 100644 --- a/src/xdist/plugin.py +++ b/src/xdist/plugin.py @@ -127,6 +127,32 @@ def pytest_addoption(parser: pytest.Parser) -> None: "(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", diff --git a/src/xdist/remote.py b/src/xdist/remote.py index 6649a0e3..436b5ddf 100644 --- a/src/xdist/remote.py +++ b/src/xdist/remote.py @@ -248,7 +248,7 @@ def pytest_collection_modifyitems( if len(mark.args) > 0 else mark.kwargs.get("name", "default") ) - gnames.add(name) + gnames.add(str(name)) if not gnames: continue item._nodeid = f"{item.nodeid}@{'_'.join(sorted(gnames))}" diff --git a/src/xdist/scheduler/loadscope.py b/src/xdist/scheduler/loadscope.py index 114561b4..0a01cb49 100644 --- a/src/xdist/scheduler/loadscope.py +++ b/src/xdist/scheduler/loadscope.py @@ -371,11 +371,15 @@ def schedule(self) -> None: work_unit = unsorted_workqueue.setdefault(scope, {}) 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 + 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) diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index ea3d4163..42d5479d 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -1254,6 +1254,24 @@ def test(i): "test_b.py::test", result.outlines ) == {"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: """Fix test suite never finishing in case all workers start with a single test (#277).""" test_file1 = """ diff --git a/testing/test_workermanage.py b/testing/test_workermanage.py index 6fb2795c..b3e8a1c7 100644 --- a/testing/test_workermanage.py +++ b/testing/test_workermanage.py @@ -206,8 +206,8 @@ def test_rsync_roots_no_roots( p = Path(p) print("remote curdir", p) assert p == dest.joinpath(config.rootpath.name) - assert p.joinpath("dir1").check() - assert p.joinpath("dir1", "file1").check() + assert p.joinpath("dir1").is_dir() + assert p.joinpath("dir1", "file1").is_file() def test_popen_rsync_subdir( self,