Skip to content

Commit 65de0ab

Browse files
Join multiple xdist_group markers (#1201)
Fixes #1200 --------- Co-authored-by: Bruno Oliveira <[email protected]>
1 parent 199f949 commit 65de0ab

File tree

4 files changed

+87
-9
lines changed

4 files changed

+87
-9
lines changed

changelog/1200.feature

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Now multiple ``xdist_group`` markers are considered when assigning tests to groups (order does not matter).
2+
3+
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.

docs/distribution.rst

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,10 @@ The test distribution algorithm is configured with the ``--dist`` command-line o
6868

6969
* ``--dist loadgroup``: Tests are grouped by the ``xdist_group`` mark. Groups are
7070
distributed to available workers as whole units. This guarantees that all
71-
tests with same ``xdist_group`` name run in the same worker.
71+
tests with same ``xdist_group`` name run in the same worker. If a test has
72+
multiple groups, they will be joined together into a new group,
73+
the order of the marks doesn't matter. This works along with marks from fixtures
74+
and from the pytestmark global variable.
7275

7376
.. code-block:: python
7477
@@ -83,6 +86,36 @@ The test distribution algorithm is configured with the ``--dist`` command-line o
8386
pass
8487
8588
This will make sure ``test1`` and ``TestA::test2`` will run in the same worker.
89+
90+
.. code-block:: python
91+
92+
@pytest.fixture(
93+
scope="session",
94+
params=[
95+
pytest.param(
96+
"chrome",
97+
marks=pytest.mark.xdist_group("chrome"),
98+
),
99+
pytest.param(
100+
"firefox",
101+
marks=pytest.mark.xdist_group("firefox"),
102+
),
103+
pytest.param(
104+
"edge",
105+
marks=pytest.mark.xdist_group("edge"),
106+
),
107+
],
108+
)
109+
def setup_container():
110+
pass
111+
112+
113+
@pytest.mark.xdist_group(name="data-store")
114+
def test_data_store(setup_container):
115+
...
116+
117+
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).
118+
86119
Tests without the ``xdist_group`` mark are distributed normally as in the ``--dist=load`` mode.
87120

88121
* ``--dist worksteal``: Initially, tests are distributed evenly among all

src/xdist/remote.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -241,15 +241,17 @@ def pytest_collection_modifyitems(
241241
# add the group name to nodeid as suffix if --dist=loadgroup
242242
if config.getvalue("loadgroup"):
243243
for item in items:
244-
mark = item.get_closest_marker("xdist_group")
245-
if not mark:
244+
gnames: set[str] = set()
245+
for mark in item.iter_markers("xdist_group"):
246+
name = (
247+
mark.args[0]
248+
if len(mark.args) > 0
249+
else mark.kwargs.get("name", "default")
250+
)
251+
gnames.add(name)
252+
if not gnames:
246253
continue
247-
gname = (
248-
mark.args[0]
249-
if len(mark.args) > 0
250-
else mark.kwargs.get("name", "default")
251-
)
252-
item._nodeid = f"{item.nodeid}@{gname}"
254+
item._nodeid = f"{item.nodeid}@{'_'.join(sorted(gnames))}"
253255

254256
@pytest.hookimpl
255257
def pytest_collection_finish(self, session: pytest.Session) -> None:

testing/acceptance_test.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1481,6 +1481,46 @@ def test_2():
14811481

14821482
assert a_1.keys() == b_1.keys() and a_2.keys() == b_2.keys()
14831483

1484+
def test_multiple_group_marks(self, pytester: pytest.Pytester) -> None:
1485+
test_file = """
1486+
import pytest
1487+
@pytest.mark.xdist_group(name="group1")
1488+
@pytest.mark.xdist_group(name="group2")
1489+
def test_1():
1490+
pass
1491+
"""
1492+
pytester.makepyfile(test_a=test_file, test_b=test_file)
1493+
result = pytester.runpytest("-n2", "--dist=loadgroup", "-v")
1494+
res = parse_tests_and_workers_from_output(result.outlines)
1495+
assert len(res) == 2
1496+
# get test names
1497+
a_1 = next(t[2] for t in res if "test_a.py::test_1" in t[2])
1498+
b_1 = next(t[2] for t in res if "test_b.py::test_1" in t[2])
1499+
# check groups
1500+
assert a_1.split("@")[1] == b_1.split("@")[1] == "group1_group2"
1501+
1502+
def test_multiple_group_order(self, pytester: pytest.Pytester) -> None:
1503+
test_file = """
1504+
import pytest
1505+
@pytest.mark.xdist_group(name="b")
1506+
@pytest.mark.xdist_group(name="d")
1507+
@pytest.mark.xdist_group(name="c")
1508+
@pytest.mark.xdist_group(name="c2")
1509+
@pytest.mark.xdist_group(name="a")
1510+
@pytest.mark.xdist_group(name="aa")
1511+
def test_1():
1512+
pass
1513+
"""
1514+
pytester.makepyfile(test_a=test_file, test_b=test_file)
1515+
result = pytester.runpytest("-n2", "--dist=loadgroup", "-v")
1516+
res = parse_tests_and_workers_from_output(result.outlines)
1517+
assert len(res) == 2
1518+
# get test names
1519+
a_1 = next(t[2] for t in res if "test_a.py::test_1" in t[2])
1520+
b_1 = next(t[2] for t in res if "test_b.py::test_1" in t[2])
1521+
# check groups, order should be sorted
1522+
assert a_1.split("@")[1] == b_1.split("@")[1] == "a_aa_b_c_c2_d"
1523+
14841524

14851525
class TestLocking:
14861526
_test_content = """

0 commit comments

Comments
 (0)