diff --git a/README.rst b/README.rst index 5768f7ca..c40c9f3e 100644 --- a/README.rst +++ b/README.rst @@ -96,6 +96,10 @@ distribution algorithm this with the ``--dist`` option. It takes these values: distributed to available workers as whole units. This guarantees that all tests in a file run in the same worker. +* ``--dist loadgroup``: Tests are grouped by 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. + Making session-scoped fixtures execute only once ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -414,3 +418,21 @@ where the configuration file was found. .. _`pytest-xdist`: http://pypi.python.org/pypi/pytest-xdist .. _`pytest-xdist repository`: https://github.com/pytest-dev/pytest-xdist .. _`pytest`: http://pytest.org + +Groups tests by xdist_group mark +--------------------------------- + +*New in version 2.4.* + +Two or more tests belonging to different classes or modules can be executed in same worker through the xdist_group marker: + +.. code-block:: python + + @pytest.mark.xdist_group(name="group1") + def test1(): + pass + + class TestA: + @pytest.mark.xdist_group("group1") + def test2(): + pass diff --git a/changelog/733.feature.rst b/changelog/733.feature.rst index 3ce8de0c..28163e79 100644 --- a/changelog/733.feature.rst +++ b/changelog/733.feature.rst @@ -1 +1 @@ -Create new dist option 'loadgroup' +New ``--dist=loadgroup`` option, which ensures all tests marked with ``@pytest.mark.xdist_group`` run in the same session/worker. Other tests run distributed as in ``--dist=load``. diff --git a/src/xdist/plugin.py b/src/xdist/plugin.py index b406d0ba..85f76e82 100644 --- a/src/xdist/plugin.py +++ b/src/xdist/plugin.py @@ -98,8 +98,7 @@ def pytest_addoption(parser): " the same scope to any available environment.\n\n" "loadfile: load balance by sending test grouped by file" " to any available environment.\n\n" - "loadgroup: load balance by sending any pending test or test group" - " to any available enviroment.\n\n" + "loadgroup: like load, but sends tests marked with 'xdist_group' to the same worker.\n\n" "(default) no: run tests inprocess, don't distribute." ), ) @@ -207,7 +206,7 @@ def pytest_configure(config): config.option.forked = True config_line = ( - "xgroup: specify group for tests should run in same session." + "xdist_group: specify group for tests should run in same session." "in relation to one another. " + "Provided by pytest-xdist." ) config.addinivalue_line("markers", config_line) diff --git a/src/xdist/remote.py b/src/xdist/remote.py index 410d3ca9..160b042a 100644 --- a/src/xdist/remote.py +++ b/src/xdist/remote.py @@ -120,15 +120,15 @@ def pytest_collection_modifyitems(self, session, config, items): # add the group name to nodeid as suffix if --dist=loadgroup if config.getvalue("loadgroup"): for item in items: - try: - mark = item.get_closest_marker("xgroup") - except AttributeError: - mark = item.get_marker("xgroup") - - if mark: - gname = mark.kwargs.get("name") - if gname: - item._nodeid = "{}@{}".format(item.nodeid, gname) + mark = item.get_closest_marker("xdist_group") + if not mark: + continue + gname = ( + mark.args[0] + if len(mark.args) > 0 + else mark.kwargs.get("name", "default") + ) + item._nodeid = "{}@{}".format(item.nodeid, gname) @pytest.hookimpl def pytest_collection_finish(self, session): @@ -250,7 +250,7 @@ def remote_initconfig(option_dict, args): def setup_config(config, basetemp): - config.option.loadgroup = True if config.getvalue("dist") == "loadgroup" else False + config.option.loadgroup = config.getvalue("dist") == "loadgroup" config.option.looponfail = False config.option.usepdb = False config.option.dist = "no" diff --git a/src/xdist/scheduler/loadgroup.py b/src/xdist/scheduler/loadgroup.py index 49951d89..072f64ab 100644 --- a/src/xdist/scheduler/loadgroup.py +++ b/src/xdist/scheduler/loadgroup.py @@ -3,22 +3,10 @@ class LoadGroupScheduling(LoadScopeScheduling): - """Implement load scheduling across nodes, but grouping test only has group mark. + """Implement load scheduling across nodes, but grouping test by xdist_group mark. - This distributes the tests collected across all nodes so each test is run - just once. All nodes collect and submit the list of tests and when all - collections are received it is verified they are identical collections. - Then the collection gets divided up in work units, grouped by group mark - (If there is no group mark, it is itself a group.), and those work units - et submitted to nodes. Whenever a node finishes an item, it calls - ``.mark_test_complete()`` which will trigger the scheduler to assign more - work units if the number of pending tests for the node falls below a low-watermark. - - When created, ``numnodes`` defines how many nodes are expected to submit a - collection. This is used to know when all nodes have finished collection. - - This class behaves very much like LoadScopeScheduling, - but with a itself or group(by marked) scope. + This class behaves very much like LoadScopeScheduling, but it groups tests by xdist_group mark + instead of the module or class to which they belong to. """ def __init__(self, config, log=None): @@ -49,10 +37,9 @@ def _split_scope(self, nodeid): example/loadsuite/test/test_gamma.py::test_beta0@gname example/loadsuite/test/test_delta.py::Gamma1::test_gamma0@gname - This function will group tests with the scope determined by splitting - the first ``@`` from the right. That is, test will be grouped in a - single work unit when they have same group name. - In the above example, scopes will be:: + This function will group tests with the scope determined by splitting the first ``@`` + from the right. That is, test will be grouped in a single work unit when they have + same group name. In the above example, scopes will be:: example/loadsuite/test/test_beta.py::test_beta0 example/loadsuite/test/test_delta.py::Delta1::test_delta0 diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index c7f99567..c1391974 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -1331,7 +1331,7 @@ def test_by_module(self, testdir): test_file = """ import pytest class TestA: - @pytest.mark.xgroup(name="xgroup") + @pytest.mark.xdist_group(name="xdist_group") @pytest.mark.parametrize('i', range(5)) def test(self, i): pass @@ -1371,12 +1371,12 @@ def test_by_class(self, testdir): test_a=""" import pytest class TestA: - @pytest.mark.xgroup(name="xgroup") + @pytest.mark.xdist_group(name="xdist_group") @pytest.mark.parametrize('i', range(10)) def test(self, i): pass class TestB: - @pytest.mark.xgroup(name="xgroup") + @pytest.mark.xdist_group(name="xdist_group") @pytest.mark.parametrize('i', range(10)) def test(self, i): pass @@ -1414,7 +1414,7 @@ def test(self, i): def test_module_single_start(self, testdir): test_file1 = """ import pytest - @pytest.mark.xgroup(name="xgroup") + @pytest.mark.xdist_group(name="xdist_group") def test(): pass """ @@ -1422,7 +1422,7 @@ def test(): import pytest def test_1(): pass - @pytest.mark.xgroup(name="xgroup") + @pytest.mark.xdist_group(name="xdist_group") def test_2(): pass """ @@ -1434,6 +1434,25 @@ def test_2(): assert a.keys() == b.keys() and b.keys() == c.keys() + def test_with_two_group_names(self, testdir): + test_file = """ + import pytest + @pytest.mark.xdist_group(name="group1") + def test_1(): + pass + @pytest.mark.xdist_group("group2") + def test_2(): + pass + """ + testdir.makepyfile(test_a=test_file, test_b=test_file) + result = testdir.runpytest("-n2", "--dist=loadgroup", "-v") + a_1 = get_workers_and_test_count_by_prefix("test_a.py::test_1", result.outlines) + a_2 = get_workers_and_test_count_by_prefix("test_a.py::test_2", result.outlines) + b_1 = get_workers_and_test_count_by_prefix("test_b.py::test_1", result.outlines) + b_2 = get_workers_and_test_count_by_prefix("test_b.py::test_2", result.outlines) + + assert a_1.keys() == b_1.keys() and a_2.keys() == b_2.keys() + class TestLocking: _test_content = """