Repository: pytest-dev/pytest-xdist
Pull request #1216 (2025-06-12 00:57:14)

Ensure all xdist group names are strings
Will come back and fill out the description. This is a possible solve of https://github.com/pytest-dev/pytest-xdist/issues/1215 for review.

The previous code before https://github.com/pytest-dev/pytest-xdist/pull/1201 called `__str__` on all group names via the f-string coercion

<!---

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.
    ```
--->


Patch:
diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
index 9363a13..4f33e07 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/.pre-commit-config.yaml b/.pre-commit-config.yaml
index e5554f6..9fb4a64 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.11"
+  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/1083.improvement.rst b/changelog/1083.improvement.rst
new file mode 100644
index 0000000..0f8dee9
--- /dev/null
+++ b/changelog/1083.improvement.rst
@@ -0,0 +1,5 @@
+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.
diff --git a/src/xdist/plugin.py b/src/xdist/plugin.py
index 0cf90f8..0a07b71 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 6649a0e..436b5dd 100644
--- a/src/xdist/remote.py
+++ b/src/xdist/remote.py
@@ -248,7 +248,7 @@ class WorkerInteractor:
                         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 114561b..0a01cb4 100644
--- a/src/xdist/scheduler/loadscope.py
+++ b/src/xdist/scheduler/loadscope.py
@@ -371,11 +371,15 @@ class LoadScopeScheduling:
             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)


Test patch:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 4be6f7d..db3840f 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/testing/acceptance_test.py b/testing/acceptance_test.py
index ea3d416..42d5479 100644
--- a/testing/acceptance_test.py
+++ b/testing/acceptance_test.py
@@ -1254,6 +1254,24 @@ class TestLoadScope:
             "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 6fb2795..b3e8a1c 100644
--- a/testing/test_workermanage.py
+++ b/testing/test_workermanage.py
@@ -206,8 +206,8 @@ class TestNodeManager:
         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,