diff --git a/changelog/1200.feature b/changelog/1200.feature new file mode 100644 index 0000000..7629758 --- /dev/null +++ b/changelog/1200.feature @@ -0,0 +1,3 @@ +Now multiple ``xdist_group`` markers are considered when assigning tests to groups (order does not matter). + +Previously, only the last marker would assign a test to a group, but now if a test has multiple ``xdist_group`` marks applied (for example via parametrization or via fixtures), they are merged to make a new group. diff --git a/docs/distribution.rst b/docs/distribution.rst index b62f787..5c62970 100644 --- a/docs/distribution.rst +++ b/docs/distribution.rst @@ -68,7 +68,10 @@ The test distribution algorithm is configured with the ``--dist`` command-line o * ``--dist loadgroup``: Tests are grouped by the ``xdist_group`` mark. Groups are distributed to available workers as whole units. This guarantees that all - tests with same ``xdist_group`` name run in the same worker. + tests with same ``xdist_group`` name run in the same worker. If a test has + multiple groups, they will be joined together into a new group, + the order of the marks doesn't matter. This works along with marks from fixtures + and from the pytestmark global variable. .. code-block:: python @@ -83,6 +86,36 @@ The test distribution algorithm is configured with the ``--dist`` command-line o pass This will make sure ``test1`` and ``TestA::test2`` will run in the same worker. + + .. code-block:: python + + @pytest.fixture( + scope="session", + params=[ + pytest.param( + "chrome", + marks=pytest.mark.xdist_group("chrome"), + ), + pytest.param( + "firefox", + marks=pytest.mark.xdist_group("firefox"), + ), + pytest.param( + "edge", + marks=pytest.mark.xdist_group("edge"), + ), + ], + ) + def setup_container(): + pass + + + @pytest.mark.xdist_group(name="data-store") + def test_data_store(setup_container): + ... + + This will generate 3 new groups: ``chrome_data-store``, ``data-store_firefox`` and ``data-store_edge`` (the markers are lexically sorted before being merged together). + Tests without the ``xdist_group`` mark are distributed normally as in the ``--dist=load`` mode. * ``--dist worksteal``: Initially, tests are distributed evenly among all diff --git a/src/xdist/remote.py b/src/xdist/remote.py index 0a30306..6649a0e 100644 --- a/src/xdist/remote.py +++ b/src/xdist/remote.py @@ -241,15 +241,17 @@ class WorkerInteractor: # add the group name to nodeid as suffix if --dist=loadgroup if config.getvalue("loadgroup"): for item in items: - mark = item.get_closest_marker("xdist_group") - if not mark: + gnames: set[str] = set() + for mark in item.iter_markers("xdist_group"): + name = ( + mark.args[0] + if len(mark.args) > 0 + else mark.kwargs.get("name", "default") + ) + gnames.add(name) + if not gnames: continue - gname = ( - mark.args[0] - if len(mark.args) > 0 - else mark.kwargs.get("name", "default") - ) - item._nodeid = f"{item.nodeid}@{gname}" + item._nodeid = f"{item.nodeid}@{'_'.join(sorted(gnames))}" @pytest.hookimpl def pytest_collection_finish(self, session: pytest.Session) -> None: diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index 3ef10cc..ea3d416 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -1481,6 +1481,46 @@ class TestGroupScope: assert a_1.keys() == b_1.keys() and a_2.keys() == b_2.keys() + def test_multiple_group_marks(self, pytester: pytest.Pytester) -> None: + test_file = """ + import pytest + @pytest.mark.xdist_group(name="group1") + @pytest.mark.xdist_group(name="group2") + def test_1(): + pass + """ + pytester.makepyfile(test_a=test_file, test_b=test_file) + result = pytester.runpytest("-n2", "--dist=loadgroup", "-v") + res = parse_tests_and_workers_from_output(result.outlines) + assert len(res) == 2 + # get test names + a_1 = next(t[2] for t in res if "test_a.py::test_1" in t[2]) + b_1 = next(t[2] for t in res if "test_b.py::test_1" in t[2]) + # check groups + assert a_1.split("@")[1] == b_1.split("@")[1] == "group1_group2" + + def test_multiple_group_order(self, pytester: pytest.Pytester) -> None: + test_file = """ + import pytest + @pytest.mark.xdist_group(name="b") + @pytest.mark.xdist_group(name="d") + @pytest.mark.xdist_group(name="c") + @pytest.mark.xdist_group(name="c2") + @pytest.mark.xdist_group(name="a") + @pytest.mark.xdist_group(name="aa") + def test_1(): + pass + """ + pytester.makepyfile(test_a=test_file, test_b=test_file) + result = pytester.runpytest("-n2", "--dist=loadgroup", "-v") + res = parse_tests_and_workers_from_output(result.outlines) + assert len(res) == 2 + # get test names + a_1 = next(t[2] for t in res if "test_a.py::test_1" in t[2]) + b_1 = next(t[2] for t in res if "test_b.py::test_1" in t[2]) + # check groups, order should be sorted + assert a_1.split("@")[1] == b_1.split("@")[1] == "a_aa_b_c_c2_d" + class TestLocking: _test_content = """