Skip to content

Commit

Permalink
Integrate isoscope scheduling and distributed sccope isolation into x…
Browse files Browse the repository at this point in the history
…dist. Not tested yet.
  • Loading branch information
Vitaly Kruglikov committed Sep 6, 2024
1 parent 7c5a664 commit e34c299
Show file tree
Hide file tree
Showing 9 changed files with 175 additions and 106 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
pytest-xdist 3.ZZZ.ZZZ (2024-zz-zz)
===============================

Features
--------
- `#1126 <https://github.com/pytest-dev/pytest-xdist/pull/1126>`_: New ``isoscope`` scheduler.

pytest-xdist 3.6.1 (2024-04-28)
===============================

Expand Down
13 changes: 13 additions & 0 deletions docs/distribution.rst
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,19 @@ The test distribution algorithm is configured with the ``--dist`` command-line o

.. _distribution modes:

* ``--dist isoscope``: Scope Isolation Scheduler. Tests are grouped by module for
test functions and by class for test methods. Tests are executed one group at a
time, distributed across available workers. This groupwise isolation guarantees
that all tests in one group complete execution before running another group of
tests. This can be useful when module-level or class-level fixtures of one group
could create undesirable side-effects for tests in other test groups, while
taking advantage of distributed execution of tests within each group. Grouping
by class takes priority over grouping by module. NOTE: the use of this scheduler
requires distributed coordination for setup and teardown such as provided by
the ``iso_scheduling`` fixture or an alternate implementation of distributed
coordination - see the ``iso_scheduling.coordinate_setup_teardown`` usage example
in iso_scheduling_plugin.py.

* ``--dist load`` **(default)**: Sends pending tests to any worker that is
available, without any guaranteed order. Scheduling can be fine-tuned with
the `--maxschedchunk` option, see output of `pytest --help`.
Expand Down
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ classifiers = [
requires-python = ">=3.8"
dependencies = [
"execnet>=2.1",
"filelock>=3.13.1",
"pytest>=7.0.0",
]
dynamic = ["version"]
Expand All @@ -47,6 +48,7 @@ Tracker = "https://github.com/pytest-dev/pytest-xdist/issues"

[project.entry-points.pytest11]
xdist = "xdist.plugin"
"xdist.iso_scheduling_plugin" = "xdist.iso_scheduling_plugin"
"xdist.looponfail" = "xdist.looponfail"

[project.optional-dependencies]
Expand Down
3 changes: 3 additions & 0 deletions src/xdist/dsession.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from xdist.remote import Producer
from xdist.remote import WorkerInfo
from xdist.scheduler import EachScheduling
from xdist.scheduler import IsoScopeScheduling
from xdist.scheduler import LoadFileScheduling
from xdist.scheduler import LoadGroupScheduling
from xdist.scheduler import LoadScheduling
Expand Down Expand Up @@ -113,6 +114,8 @@ def pytest_xdist_make_scheduler(
dist = config.getvalue("dist")
if dist == "each":
return EachScheduling(config, log)
if dist == "isoscope":
return IsoScopeScheduling(config, log)
if dist == "load":
return LoadScheduling(config, log)
if dist == "loadscope":
Expand Down
90 changes: 46 additions & 44 deletions src/xdist/iso_scheduling_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

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

from utils.common.parallel_mono_scope_utils import (
ParallelMonoScopeFixture,
from xdist.iso_scheduling_utils import (
IsoSchedulingFixture,
DistributedSetupCoordinator,
DistributedSetupContext,
DistributedTeardownContext,
Expand All @@ -63,16 +63,24 @@


@pytest.fixture(scope='session')
def parallel_mono_scope(
def iso_scheduling(
tmp_path_factory: pytest.TempPathFactory,
testrun_uid: str,
worker_id: str
) -> ParallelMonoScopeFixture:
) -> IsoSchedulingFixture:
"""A session-scoped pytest fixture for coordinating setup/teardown of test
scope/class which is executing in the parallel_mono_scope Test Distribution
Mode.
scope/class which is executing under isoscope scheduling.
NOTE: Each XDist remote worker is running its own Pytest Session.
Based on the filelock idea described in section
"Making session-scoped fixtures execute only once" of
https://pytest-xdist.readthedocs.io/en/stable/how-to.html.
NOTE: Each XDist remote worker is running its own Pytest Session, so we want
only the worker that starts its session first to execute the setup logic and
only the worker that finishes its session last to execute the teardown logic
using a form of distributed coordination. This way, setup is executed exactly
once before any worker executes any of the scope's tests, and teardown is
executed only after the last worker finishes test execution.
USAGE EXAMPLE:
Expand All @@ -82,24 +90,23 @@ def parallel_mono_scope(
import pytest
if TYPE_CHECKING:
from utils.common.parallel_mono_scope_utils import (
ParallelMonoScopeFixture,
from xdist.iso_scheduling_utils import (
IsoSchedulingFixture,
DistributedSetupContext,
DistributedTeardownContext
)
@pytest.mark.parallel_mono_scope
class TestDeng12345ParallelMonoScope:
class TestSomething:
@classmethod
@pytest.fixture(scope='class', autouse=True)
def distributed_setup_and_teardown(
cls,
parallel_mono_scope: ParallelMonoScopeFixture:
iso_scheduling: IsoSchedulingFixture:
request: pytest.FixtureRequest):
# Distributed Setup and Teardown
with parallel_mono_scope.coordinate_setup_teardown(
with iso_scheduling.coordinate_setup_teardown(
setup_request=request) as coordinator:
# Distributed Setup
coordinator.maybe_call_setup(cls.patch_system_under_test)
Expand All @@ -126,12 +133,13 @@ def revert_system_under_test(
# Fetch state from `teardown_context.client_dir` and revert
# changes made by `patch_system_under_test()`.
perms, tc_ids = generate_tests(
os.path.realpath(__file__),
TestDistributionModeEnum.PARALLEL_MONO_SCOPE)
def test_case1(self)
...
def test_case2(self)
...
@pytest.mark.parametrize('test_data', perms, ids=tc_ids)
def test_case(self, test_data: dict[str, dict])
def test_case3(self)
...
```
Expand All @@ -146,17 +154,17 @@ def test_case(self, test_data: dict[str, dict])
yields an instance of `DistributedSetupCoordinator` for the current
Pytest Session.
"""
return _ParallelMonoScopeFixtureImpl(tmp_path_factory=tmp_path_factory,
testrun_uid=testrun_uid,
worker_id=worker_id)
return _IsoSchedulingFixtureImpl(tmp_path_factory=tmp_path_factory,
testrun_uid=testrun_uid,
worker_id=worker_id)


class _ParallelMonoScopeFixtureImpl(ParallelMonoScopeFixture):
class _IsoSchedulingFixtureImpl(IsoSchedulingFixture):
"""Context manager yielding a new instance of the implementation of the
`DistributedSetupCoordinator` interface.
An instance of _ParallelMonoScopeFixtureImpl is returned by our pytest
fixture `parallel_mono_scope`.
An instance of _IsoSchedulingFixtureImpl is returned by our pytest
fixture `iso_scheduling`.
"""
# pylint: disable=too-few-public-methods

Expand Down Expand Up @@ -206,11 +214,11 @@ def coordinate_setup_teardown(


class _DistributedSetupCoordinatorImpl(DistributedSetupCoordinator):
"""Distributed scope/class setup/teardown coordination for the
`parallel_mono_scope` Test Distribution Mode.
"""Distributed scope/class setup/teardown coordination for isoscope
scheduling.
NOTE: do not instantiate this class directly. Use the
`parallel_mono_scope` fixture instead!
`iso_scheduling` fixture instead!
"""
_DISTRIBUTED_SETUP_ROOT_DIR_LINK_NAME = 'distributed_setup'
Expand Down Expand Up @@ -257,7 +265,7 @@ def maybe_call_setup(
Process-safe.
Call `maybe_call_setup` from the pytest setup-teardown fixture of your
`PARALLEL_MONO_SCOPE` test (typically test class) if it needs to
isoscope-scheduled test (typically test class) if it needs to
initialize a resource which is common to all of its test cases which may
be executing in different XDist worker processes (such as a subnet in
`subnet.xml`).
Expand All @@ -272,8 +280,7 @@ def maybe_call_setup(
:return: An instance of `DistributedSetupContext` which MUST be passed
in the corresponding call to `maybe_call_teardown`.
:raise parallel_mono_scope.CoordinationTimeoutError: If attempt to
acquire the lock times out.
:raise CoordinationTimeoutError: If attempt to acquire the lock times out.
"""
# `maybe_call_setup()` may be called only once per instance of
# `_SetupCoordinator`
Expand Down Expand Up @@ -307,7 +314,7 @@ def maybe_call_teardown(
tests for your test scope. Process-safe.
Call `maybe_call_teardown` from the pytest setup-teardown fixture of
your `PARALLEL_MONO_SCOPE` test (typically test class) if it needs to
your isoscope-scheduled test (typically test class) if it needs to
initialize a resource which is common to all of its test cases which may
be executing in different XDist worker processes (such as a subnet in
`subnet.xml`).
Expand All @@ -320,8 +327,7 @@ def maybe_call_teardown(
invoked.
:param timeout: Lock acquisition timeout in seconds
:raise parallel_mono_scope.CoordinationTimeoutError: If attempt to
acquire the lock times out.
:raise CoordinationTimeoutError: If attempt to acquire the lock times out.
"""
# Make sure `maybe_call_setup()` was already called on this instance
# of `_SetupCoordinator`
Expand Down Expand Up @@ -359,8 +365,7 @@ def wrapper(*args, **kwargs):

class _DistributedSetupCoordinationImpl:
"""Low-level implementation of Context Managers for Coordinating
Distributed Setup and Teardown for the `parallel_mono_scope`
Test Distribution Mode.
Distributed Setup and Teardown for users of isoscope scheduling.
"""
_ROOT_STATE_FILE_NAME = 'root_state.json'
_ROOT_LOCK_FILE_NAME = 'lock'
Expand Down Expand Up @@ -426,7 +431,7 @@ def acquire_distributed_setup(
timeout: float
) -> Generator[DistributedSetupContext, None, None]:
"""Low-level implementation of Context Manager for Coordinating
Distributed Setup for the `parallel_mono_scope` Test Distribution Mode.
Distributed Setup for isoscope scheduling.
:param root_context_dir: Scope/class-specific root directory for
saving this context manager's state. This directory is common to
Expand All @@ -436,8 +441,7 @@ def acquire_distributed_setup(
directly by the calling setup-teardown fixture.
:param timeout: Lock acquisition timeout in seconds
:raise parallel_mono_scope.CoordinationTimeoutError: If attempt to
acquire the lock times out.
:raise CoordinationTimeoutError: If attempt to acquire the lock times out.
"""
#
# Before control passes to the managed code block
Expand Down Expand Up @@ -502,16 +506,14 @@ def acquire_distributed_teardown(
timeout: float
) -> Generator[DistributedTeardownContext, None, None]:
"""Low-level implementation of Context Manager for Coordinating
Distributed Teardown for the `parallel_mono_scope` Test Distribution
Mode.
Distributed Teardown for the isoscope scheduling.
:param setup_context: The instance of `DistributedSetupContext` that was
yielded by the corresponding use of the
`_distributed_setup_permission` context manager.
:param timeout: Lock acquisition timeout in seconds
:raise parallel_mono_scope.CoordinationTimeoutError: If attempt to
acquire the lock times out.
:raise CoordinationTimeoutError: If attempt to acquire the lock times out.
"""
#
# Before control passes to the managed code block
Expand Down
Loading

0 comments on commit e34c299

Please sign in to comment.