Skip to content

Commit e34c299

Browse files
author
Vitaly Kruglikov
committed
Integrate isoscope scheduling and distributed sccope isolation into xdist. Not tested yet.
1 parent 7c5a664 commit e34c299

9 files changed

+175
-106
lines changed

CHANGELOG.rst

+7
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
pytest-xdist 3.ZZZ.ZZZ (2024-zz-zz)
2+
===============================
3+
4+
Features
5+
--------
6+
- `#1126 <https://github.com/pytest-dev/pytest-xdist/pull/1126>`_: New ``isoscope`` scheduler.
7+
18
pytest-xdist 3.6.1 (2024-04-28)
29
===============================
310

docs/distribution.rst

+13
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,19 @@ The test distribution algorithm is configured with the ``--dist`` command-line o
4949

5050
.. _distribution modes:
5151

52+
* ``--dist isoscope``: Scope Isolation Scheduler. Tests are grouped by module for
53+
test functions and by class for test methods. Tests are executed one group at a
54+
time, distributed across available workers. This groupwise isolation guarantees
55+
that all tests in one group complete execution before running another group of
56+
tests. This can be useful when module-level or class-level fixtures of one group
57+
could create undesirable side-effects for tests in other test groups, while
58+
taking advantage of distributed execution of tests within each group. Grouping
59+
by class takes priority over grouping by module. NOTE: the use of this scheduler
60+
requires distributed coordination for setup and teardown such as provided by
61+
the ``iso_scheduling`` fixture or an alternate implementation of distributed
62+
coordination - see the ``iso_scheduling.coordinate_setup_teardown`` usage example
63+
in iso_scheduling_plugin.py.
64+
5265
* ``--dist load`` **(default)**: Sends pending tests to any worker that is
5366
available, without any guaranteed order. Scheduling can be fine-tuned with
5467
the `--maxschedchunk` option, see output of `pytest --help`.

pyproject.toml

+2
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ classifiers = [
3434
requires-python = ">=3.8"
3535
dependencies = [
3636
"execnet>=2.1",
37+
"filelock>=3.13.1",
3738
"pytest>=7.0.0",
3839
]
3940
dynamic = ["version"]
@@ -47,6 +48,7 @@ Tracker = "https://github.com/pytest-dev/pytest-xdist/issues"
4748

4849
[project.entry-points.pytest11]
4950
xdist = "xdist.plugin"
51+
"xdist.iso_scheduling_plugin" = "xdist.iso_scheduling_plugin"
5052
"xdist.looponfail" = "xdist.looponfail"
5153

5254
[project.optional-dependencies]

src/xdist/dsession.py

+3
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from xdist.remote import Producer
1616
from xdist.remote import WorkerInfo
1717
from xdist.scheduler import EachScheduling
18+
from xdist.scheduler import IsoScopeScheduling
1819
from xdist.scheduler import LoadFileScheduling
1920
from xdist.scheduler import LoadGroupScheduling
2021
from xdist.scheduler import LoadScheduling
@@ -113,6 +114,8 @@ def pytest_xdist_make_scheduler(
113114
dist = config.getvalue("dist")
114115
if dist == "each":
115116
return EachScheduling(config, log)
117+
if dist == "isoscope":
118+
return IsoScopeScheduling(config, log)
116119
if dist == "load":
117120
return LoadScheduling(config, log)
118121
if dist == "loadscope":

src/xdist/iso_scheduling_plugin.py

+46-44
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,9 @@
2222
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
2323
# SOFTWARE.
2424

25-
"""Pytest Fixtures for supporting the PARALLEL_MONO_SCOPE Test Distribution Mode.
25+
"""Pytest Fixtures for supporting users of isoscope scheduling.
2626
27-
NOTE: These fixtures are NOT compatible with any other Test Distribution Modes.
27+
NOTE: These fixtures are NOT compatible with any other xdist schedulers.
2828
2929
NOTE: DO NOT IMPORT this module. It needs to be loaded via pytest's
3030
`conftest.pytest_plugins` mechanism. Pytest doc discourages importing fixtures
@@ -46,8 +46,8 @@
4646
import filelock
4747
import pytest
4848

49-
from utils.common.parallel_mono_scope_utils import (
50-
ParallelMonoScopeFixture,
49+
from xdist.iso_scheduling_utils import (
50+
IsoSchedulingFixture,
5151
DistributedSetupCoordinator,
5252
DistributedSetupContext,
5353
DistributedTeardownContext,
@@ -63,16 +63,24 @@
6363

6464

6565
@pytest.fixture(scope='session')
66-
def parallel_mono_scope(
66+
def iso_scheduling(
6767
tmp_path_factory: pytest.TempPathFactory,
6868
testrun_uid: str,
6969
worker_id: str
70-
) -> ParallelMonoScopeFixture:
70+
) -> IsoSchedulingFixture:
7171
"""A session-scoped pytest fixture for coordinating setup/teardown of test
72-
scope/class which is executing in the parallel_mono_scope Test Distribution
73-
Mode.
72+
scope/class which is executing under isoscope scheduling.
7473
75-
NOTE: Each XDist remote worker is running its own Pytest Session.
74+
Based on the filelock idea described in section
75+
"Making session-scoped fixtures execute only once" of
76+
https://pytest-xdist.readthedocs.io/en/stable/how-to.html.
77+
78+
NOTE: Each XDist remote worker is running its own Pytest Session, so we want
79+
only the worker that starts its session first to execute the setup logic and
80+
only the worker that finishes its session last to execute the teardown logic
81+
using a form of distributed coordination. This way, setup is executed exactly
82+
once before any worker executes any of the scope's tests, and teardown is
83+
executed only after the last worker finishes test execution.
7684
7785
USAGE EXAMPLE:
7886
@@ -82,24 +90,23 @@ def parallel_mono_scope(
8290
import pytest
8391
8492
if TYPE_CHECKING:
85-
from utils.common.parallel_mono_scope_utils import (
86-
ParallelMonoScopeFixture,
93+
from xdist.iso_scheduling_utils import (
94+
IsoSchedulingFixture,
8795
DistributedSetupContext,
8896
DistributedTeardownContext
8997
)
9098
91-
@pytest.mark.parallel_mono_scope
92-
class TestDeng12345ParallelMonoScope:
99+
class TestSomething:
93100
94101
@classmethod
95102
@pytest.fixture(scope='class', autouse=True)
96103
def distributed_setup_and_teardown(
97104
cls,
98-
parallel_mono_scope: ParallelMonoScopeFixture:
105+
iso_scheduling: IsoSchedulingFixture:
99106
request: pytest.FixtureRequest):
100107
101108
# Distributed Setup and Teardown
102-
with parallel_mono_scope.coordinate_setup_teardown(
109+
with iso_scheduling.coordinate_setup_teardown(
103110
setup_request=request) as coordinator:
104111
# Distributed Setup
105112
coordinator.maybe_call_setup(cls.patch_system_under_test)
@@ -126,12 +133,13 @@ def revert_system_under_test(
126133
# Fetch state from `teardown_context.client_dir` and revert
127134
# changes made by `patch_system_under_test()`.
128135
129-
perms, tc_ids = generate_tests(
130-
os.path.realpath(__file__),
131-
TestDistributionModeEnum.PARALLEL_MONO_SCOPE)
136+
def test_case1(self)
137+
...
138+
139+
def test_case2(self)
140+
...
132141
133-
@pytest.mark.parametrize('test_data', perms, ids=tc_ids)
134-
def test_case(self, test_data: dict[str, dict])
142+
def test_case3(self)
135143
...
136144
```
137145
@@ -146,17 +154,17 @@ def test_case(self, test_data: dict[str, dict])
146154
yields an instance of `DistributedSetupCoordinator` for the current
147155
Pytest Session.
148156
"""
149-
return _ParallelMonoScopeFixtureImpl(tmp_path_factory=tmp_path_factory,
150-
testrun_uid=testrun_uid,
151-
worker_id=worker_id)
157+
return _IsoSchedulingFixtureImpl(tmp_path_factory=tmp_path_factory,
158+
testrun_uid=testrun_uid,
159+
worker_id=worker_id)
152160

153161

154-
class _ParallelMonoScopeFixtureImpl(ParallelMonoScopeFixture):
162+
class _IsoSchedulingFixtureImpl(IsoSchedulingFixture):
155163
"""Context manager yielding a new instance of the implementation of the
156164
`DistributedSetupCoordinator` interface.
157165
158-
An instance of _ParallelMonoScopeFixtureImpl is returned by our pytest
159-
fixture `parallel_mono_scope`.
166+
An instance of _IsoSchedulingFixtureImpl is returned by our pytest
167+
fixture `iso_scheduling`.
160168
"""
161169
# pylint: disable=too-few-public-methods
162170

@@ -206,11 +214,11 @@ def coordinate_setup_teardown(
206214

207215

208216
class _DistributedSetupCoordinatorImpl(DistributedSetupCoordinator):
209-
"""Distributed scope/class setup/teardown coordination for the
210-
`parallel_mono_scope` Test Distribution Mode.
217+
"""Distributed scope/class setup/teardown coordination for isoscope
218+
scheduling.
211219
212220
NOTE: do not instantiate this class directly. Use the
213-
`parallel_mono_scope` fixture instead!
221+
`iso_scheduling` fixture instead!
214222
215223
"""
216224
_DISTRIBUTED_SETUP_ROOT_DIR_LINK_NAME = 'distributed_setup'
@@ -257,7 +265,7 @@ def maybe_call_setup(
257265
Process-safe.
258266
259267
Call `maybe_call_setup` from the pytest setup-teardown fixture of your
260-
`PARALLEL_MONO_SCOPE` test (typically test class) if it needs to
268+
isoscope-scheduled test (typically test class) if it needs to
261269
initialize a resource which is common to all of its test cases which may
262270
be executing in different XDist worker processes (such as a subnet in
263271
`subnet.xml`).
@@ -272,8 +280,7 @@ def maybe_call_setup(
272280
:return: An instance of `DistributedSetupContext` which MUST be passed
273281
in the corresponding call to `maybe_call_teardown`.
274282
275-
:raise parallel_mono_scope.CoordinationTimeoutError: If attempt to
276-
acquire the lock times out.
283+
:raise CoordinationTimeoutError: If attempt to acquire the lock times out.
277284
"""
278285
# `maybe_call_setup()` may be called only once per instance of
279286
# `_SetupCoordinator`
@@ -307,7 +314,7 @@ def maybe_call_teardown(
307314
tests for your test scope. Process-safe.
308315
309316
Call `maybe_call_teardown` from the pytest setup-teardown fixture of
310-
your `PARALLEL_MONO_SCOPE` test (typically test class) if it needs to
317+
your isoscope-scheduled test (typically test class) if it needs to
311318
initialize a resource which is common to all of its test cases which may
312319
be executing in different XDist worker processes (such as a subnet in
313320
`subnet.xml`).
@@ -320,8 +327,7 @@ def maybe_call_teardown(
320327
invoked.
321328
:param timeout: Lock acquisition timeout in seconds
322329
323-
:raise parallel_mono_scope.CoordinationTimeoutError: If attempt to
324-
acquire the lock times out.
330+
:raise CoordinationTimeoutError: If attempt to acquire the lock times out.
325331
"""
326332
# Make sure `maybe_call_setup()` was already called on this instance
327333
# of `_SetupCoordinator`
@@ -359,8 +365,7 @@ def wrapper(*args, **kwargs):
359365

360366
class _DistributedSetupCoordinationImpl:
361367
"""Low-level implementation of Context Managers for Coordinating
362-
Distributed Setup and Teardown for the `parallel_mono_scope`
363-
Test Distribution Mode.
368+
Distributed Setup and Teardown for users of isoscope scheduling.
364369
"""
365370
_ROOT_STATE_FILE_NAME = 'root_state.json'
366371
_ROOT_LOCK_FILE_NAME = 'lock'
@@ -426,7 +431,7 @@ def acquire_distributed_setup(
426431
timeout: float
427432
) -> Generator[DistributedSetupContext, None, None]:
428433
"""Low-level implementation of Context Manager for Coordinating
429-
Distributed Setup for the `parallel_mono_scope` Test Distribution Mode.
434+
Distributed Setup for isoscope scheduling.
430435
431436
:param root_context_dir: Scope/class-specific root directory for
432437
saving this context manager's state. This directory is common to
@@ -436,8 +441,7 @@ def acquire_distributed_setup(
436441
directly by the calling setup-teardown fixture.
437442
:param timeout: Lock acquisition timeout in seconds
438443
439-
:raise parallel_mono_scope.CoordinationTimeoutError: If attempt to
440-
acquire the lock times out.
444+
:raise CoordinationTimeoutError: If attempt to acquire the lock times out.
441445
"""
442446
#
443447
# Before control passes to the managed code block
@@ -502,16 +506,14 @@ def acquire_distributed_teardown(
502506
timeout: float
503507
) -> Generator[DistributedTeardownContext, None, None]:
504508
"""Low-level implementation of Context Manager for Coordinating
505-
Distributed Teardown for the `parallel_mono_scope` Test Distribution
506-
Mode.
509+
Distributed Teardown for the isoscope scheduling.
507510
508511
:param setup_context: The instance of `DistributedSetupContext` that was
509512
yielded by the corresponding use of the
510513
`_distributed_setup_permission` context manager.
511514
:param timeout: Lock acquisition timeout in seconds
512515
513-
:raise parallel_mono_scope.CoordinationTimeoutError: If attempt to
514-
acquire the lock times out.
516+
:raise CoordinationTimeoutError: If attempt to acquire the lock times out.
515517
"""
516518
#
517519
# Before control passes to the managed code block

0 commit comments

Comments
 (0)