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/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) 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 = """