Join multiple xdist_group markers (#1201)
Fixes #1200 --------- Co-authored-by: Bruno Oliveira <bruno@soliv.dev>
This commit is contained in:
3
changelog/1200.feature
Normal file
3
changelog/1200.feature
Normal file
@@ -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.
|
||||||
@@ -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
|
* ``--dist loadgroup``: Tests are grouped by the ``xdist_group`` mark. Groups are
|
||||||
distributed to available workers as whole units. This guarantees that all
|
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
|
.. code-block:: python
|
||||||
|
|
||||||
@@ -83,6 +86,36 @@ The test distribution algorithm is configured with the ``--dist`` command-line o
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
This will make sure ``test1`` and ``TestA::test2`` will run in the same worker.
|
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.
|
Tests without the ``xdist_group`` mark are distributed normally as in the ``--dist=load`` mode.
|
||||||
|
|
||||||
* ``--dist worksteal``: Initially, tests are distributed evenly among all
|
* ``--dist worksteal``: Initially, tests are distributed evenly among all
|
||||||
|
|||||||
@@ -241,15 +241,17 @@ class WorkerInteractor:
|
|||||||
# add the group name to nodeid as suffix if --dist=loadgroup
|
# add the group name to nodeid as suffix if --dist=loadgroup
|
||||||
if config.getvalue("loadgroup"):
|
if config.getvalue("loadgroup"):
|
||||||
for item in items:
|
for item in items:
|
||||||
mark = item.get_closest_marker("xdist_group")
|
gnames: set[str] = set()
|
||||||
if not mark:
|
for mark in item.iter_markers("xdist_group"):
|
||||||
continue
|
name = (
|
||||||
gname = (
|
|
||||||
mark.args[0]
|
mark.args[0]
|
||||||
if len(mark.args) > 0
|
if len(mark.args) > 0
|
||||||
else mark.kwargs.get("name", "default")
|
else mark.kwargs.get("name", "default")
|
||||||
)
|
)
|
||||||
item._nodeid = f"{item.nodeid}@{gname}"
|
gnames.add(name)
|
||||||
|
if not gnames:
|
||||||
|
continue
|
||||||
|
item._nodeid = f"{item.nodeid}@{'_'.join(sorted(gnames))}"
|
||||||
|
|
||||||
@pytest.hookimpl
|
@pytest.hookimpl
|
||||||
def pytest_collection_finish(self, session: pytest.Session) -> None:
|
def pytest_collection_finish(self, session: pytest.Session) -> None:
|
||||||
|
|||||||
@@ -1481,6 +1481,46 @@ class TestGroupScope:
|
|||||||
|
|
||||||
assert a_1.keys() == b_1.keys() and a_2.keys() == b_2.keys()
|
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:
|
class TestLocking:
|
||||||
_test_content = """
|
_test_content = """
|
||||||
|
|||||||
Reference in New Issue
Block a user