Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -80,10 +80,9 @@ test = [
"moto[server]>=5.0,<6",
"openai-responses>=0.11.4,<1",
"optuna>=3.0,<5",
"pytest>=8.3,<9.1.0",
"pytest-asyncio>=1.0,<1.4.0",
"pytest>=8.3,<10",
"pytest-asyncio>=1.4,<2",
"pytest-benchmark>=5.1.0",
"pytest-cases>=3.8,<4",
"pytest-env>=1.1,<2",
"pytest-rerunfailures>=15.0,<17",
"ray[default,tune]>=2.40.0,<3",
Expand Down
47 changes: 35 additions & 12 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,15 @@
import typing as _t
from unittest.mock import patch

import msgspec
import pytest
import pytest_asyncio
import pytest_cases
from that_depends import ContextScopes, container_context
import uvloop

from plugboard.component import Component, IOController as IO
from plugboard.component.io_controller import IOStreamClosedError
from plugboard.connector import ZMQConnector
from plugboard.connector import Connector
from plugboard.schemas import Status
from plugboard.utils.di import DI
from plugboard.utils.settings import Settings
Expand All @@ -31,7 +31,7 @@ def override_settings(settings: Settings) -> _t.Iterator[None]:
DI.settings.reset_override_sync()


@pytest.hookimpl(optionalhook=True)
@pytest.hookimpl
def pytest_asyncio_loop_factories() -> dict[str, _t.Callable[[], asyncio.AbstractEventLoop]]:
"""Configure pytest-asyncio to create event loops with uvloop."""
return {"uvloop": uvloop.new_event_loop}
Expand Down Expand Up @@ -79,16 +79,39 @@ async def DI_teardown() -> _t.AsyncGenerator[None, None]:
await DI.tear_down()


@pytest_cases.fixture
@pytest_cases.parametrize(zmq_pubsub_proxy=[False, True])
def zmq_connector_cls(zmq_pubsub_proxy: bool) -> _t.Iterator[_t.Type[ZMQConnector]]:
"""Returns the ZMQConnector class with the specified proxy setting.
class ConnectorCase(msgspec.Struct, frozen=True):
"""Connector implementation and optional ZMQ proxy setting for a test case."""

Overrides settings to control the proxy setting without mutating process env.
"""
testing_settings = Settings.model_validate({"flags": {"zmq_pubsub_proxy": zmq_pubsub_proxy}})
with override_settings(testing_settings):
yield ZMQConnector
connector_cls: type[Connector]
zmq_pubsub_proxy: bool | None = None


def connector_case_id(value: object) -> str | None:
"""Name connector cases while leaving other parameter IDs to pytest."""
if not isinstance(value, ConnectorCase):
return None
name = value.connector_cls.__name__
if value.zmq_pubsub_proxy is not None:
name += f"-zmq_pubsub_proxy={value.zmq_pubsub_proxy}"
return name


@contextmanager
def configured_connector(case: ConnectorCase) -> _t.Iterator[type[Connector]]:
"""Apply a connector case's settings until fixture teardown, including on failure."""
if case.zmq_pubsub_proxy is None:
yield case.connector_cls
else:
settings = Settings.model_validate({"flags": {"zmq_pubsub_proxy": case.zmq_pubsub_proxy}})
with override_settings(settings):
yield case.connector_cls


@pytest.fixture
def connector_cls(request: pytest.FixtureRequest) -> _t.Iterator[type[Connector]]:
"""Resolve connector cases supplied through indirect parametrization."""
with configured_connector(request.param) as cls:
yield cls


class ComponentTestHelper(Component, ABC):
Expand Down
51 changes: 25 additions & 26 deletions tests/integration/test_channel.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@

from plugboard_schemas.connector import ConnectorMode, ConnectorSpec
import pytest
import pytest_cases

from plugboard.connector import (
Connector,
Expand All @@ -16,43 +15,43 @@
from plugboard.connector.redis_channel import RedisConnector
from plugboard.utils import DI
from plugboard.utils.settings import Settings
from tests.conftest import override_settings
from tests.conftest import ConnectorCase, configured_connector, connector_case_id
from tests.unit.test_channel import ( # noqa: F401
TEST_ITEMS,
test_channel,
test_multiprocessing_channel,
)


@pytest_cases.fixture
@pytest_cases.parametrize(zmq_pubsub_proxy=[True])
def zmq_connector_cls(zmq_pubsub_proxy: bool) -> _t.Iterator[_t.Type[ZMQConnector]]:
"""Returns the ZMQConnector class with the specified proxy setting.

Overrides settings to control the proxy setting without mutating process env.
"""
testing_settings = Settings.model_validate({"flags": {"zmq_pubsub_proxy": zmq_pubsub_proxy}})
with override_settings(testing_settings):
yield ZMQConnector


@pytest_cases.fixture
@pytest_cases.parametrize("_connector_cls", [RabbitMQConnector, zmq_connector_cls, RedisConnector])
def connector_cls(_connector_cls: type[Connector]) -> type[Connector]:
"""Fixture for `Connector` of various types."""
return _connector_cls
@pytest.fixture(
params=[
ConnectorCase(RabbitMQConnector),
ConnectorCase(ZMQConnector, True),
ConnectorCase(RedisConnector),
],
ids=connector_case_id,
)
def connector_cls(request: pytest.FixtureRequest) -> _t.Iterator[type[Connector]]:
"""Configure each connector variant for this test module."""
with configured_connector(request.param) as cls:
yield cls


@pytest_cases.fixture
@pytest_cases.parametrize(
"_connector_cls_mp", [RabbitMQConnector, zmq_connector_cls, RedisConnector]
@pytest.fixture(
params=[
ConnectorCase(RabbitMQConnector),
ConnectorCase(ZMQConnector, True),
ConnectorCase(RedisConnector),
],
ids=connector_case_id,
)
def connector_cls_mp(_connector_cls_mp: type[Connector]) -> type[Connector]:
"""Fixture for `Connector` of various types for use in multiprocess context."""
return _connector_cls_mp
def connector_cls_mp(request: pytest.FixtureRequest) -> _t.Iterator[type[Connector]]:
"""Configure each connector variant for this test module."""
with configured_connector(request.param) as cls:
yield cls


@pytest_cases.parametrize("connector_cls", [RabbitMQConnector, RedisConnector])
@pytest.mark.parametrize("connector_cls", [RabbitMQConnector, RedisConnector])
async def test_channel_broker_url_unset(connector_cls: type[Connector], job_id_ctx: str) -> None:
"""Test that attempting to connect a channel without the broker URL set raises an error."""
spec = ConnectorSpec(mode=ConnectorMode.PIPELINE, source="test.send", target="test.recv")
Expand Down
5 changes: 2 additions & 3 deletions tests/integration/test_component_decorator.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
import typing as _t

import pytest
import pytest_cases

from plugboard.component import IOController as IO
from plugboard.component.utils import component
Expand Down Expand Up @@ -83,7 +82,7 @@ async def step(self) -> None:


@pytest.mark.asyncio
@pytest_cases.parametrize(
@pytest.mark.parametrize(
"process_cls, connector_cls",
[
(LocalProcess, AsyncioConnector),
Expand Down Expand Up @@ -127,7 +126,7 @@ async def test_process_with_decorated_components(


@pytest.mark.asyncio
@pytest_cases.parametrize(
@pytest.mark.parametrize(
"process_cls, connector_cls",
[
(LocalProcess, AsyncioConnector),
Expand Down
22 changes: 14 additions & 8 deletions tests/integration/test_component_event_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,13 @@
from pydantic import BaseModel
import pytest
import pytest_asyncio
import pytest_cases

from plugboard.component import Component, IOController
from plugboard.connector import AsyncioConnector, Connector, ConnectorBuilder
from plugboard.connector import AsyncioConnector, Connector, ConnectorBuilder, ZMQConnector
from plugboard.events import Event
from plugboard.events.event import StopEvent
from plugboard.schemas import ConnectorSpec
from tests.conftest import zmq_connector_cls
from tests.conftest import ConnectorCase, configured_connector, connector_case_id


class EventTypeAData(BaseModel):
Expand Down Expand Up @@ -77,11 +76,18 @@ async def event_B_handler(self, evt: EventTypeB) -> None:
self._event_B_count += evt.data.y


@pytest_cases.fixture(scope="function")
@pytest_cases.parametrize("_connector_cls", [AsyncioConnector, zmq_connector_cls])
def connector_cls(_connector_cls: _t.Type[Connector]) -> _t.Type[Connector]:
"""Returns a `Connector` class."""
return _connector_cls
@pytest.fixture(
params=[
ConnectorCase(AsyncioConnector),
ConnectorCase(ZMQConnector, False),
ConnectorCase(ZMQConnector, True),
],
ids=connector_case_id,
)
def connector_cls(request: pytest.FixtureRequest) -> _t.Iterator[type[Connector]]:
"""Configure each connector variant for this test module."""
with configured_connector(request.param) as cls:
yield cls


@pytest.fixture
Expand Down
57 changes: 25 additions & 32 deletions tests/integration/test_connector_pubsub.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,14 @@
import typing as _t

import pytest
import pytest_cases

from plugboard.connector import (
Connector,
RabbitMQConnector,
ZMQConnector,
)
from plugboard.connector.redis_channel import RedisConnector
from plugboard.utils.settings import Settings
from tests.conftest import override_settings
from tests.conftest import ConnectorCase, configured_connector, connector_case_id
from tests.unit.test_connector_pubsub import ( # noqa: F401
_HASH_SEED,
TEST_ITEMS,
Expand All @@ -22,31 +20,26 @@
)


@pytest_cases.fixture
@pytest_cases.parametrize(zmq_pubsub_proxy=[True])
def zmq_connector_cls(zmq_pubsub_proxy: bool) -> _t.Iterator[_t.Type[ZMQConnector]]:
"""Returns the ZMQConnector class with the specified proxy setting.

Overrides settings to control the proxy setting without mutating process env.
"""
testing_settings = Settings.model_validate({"flags": {"zmq_pubsub_proxy": zmq_pubsub_proxy}})
with override_settings(testing_settings):
yield ZMQConnector


@pytest_cases.fixture
@pytest_cases.parametrize(_connector_cls=[RabbitMQConnector, zmq_connector_cls, RedisConnector])
def connector_cls(_connector_cls: type[Connector]) -> type[Connector]:
"""Fixture for `Connector` of various types."""
return _connector_cls
@pytest.fixture(
params=[
ConnectorCase(RabbitMQConnector),
ConnectorCase(ZMQConnector, True),
ConnectorCase(RedisConnector),
],
ids=connector_case_id,
)
def connector_cls(request: pytest.FixtureRequest) -> _t.Iterator[type[Connector]]:
"""Configure each connector variant for this test module."""
with configured_connector(request.param) as cls:
yield cls


@pytest.mark.asyncio
@pytest_cases.parametrize(
"connector_cls, num_subscribers, num_messages",
@pytest.mark.parametrize(
"num_subscribers, num_messages",
[
(connector_cls, 1, 100),
(connector_cls, 10, 100),
(1, 100),
(10, 100),
],
)
async def test_pubsub_channel_single_publisher(
Expand All @@ -61,11 +54,11 @@ async def test_pubsub_channel_single_publisher(


@pytest.mark.asyncio
@pytest_cases.parametrize(
"connector_cls, num_publishers, num_subscribers, num_messages",
@pytest.mark.parametrize(
"num_publishers, num_subscribers, num_messages",
[
(connector_cls, 10, 1, 100),
(connector_cls, 10, 10, 100),
(10, 1, 100),
(10, 10, 100),
],
)
async def test_pubsub_channel_multiple_publishers(
Expand All @@ -86,11 +79,11 @@ async def test_pubsub_channel_multiple_publishers(


@pytest.mark.asyncio
@pytest_cases.parametrize(
"connector_cls, num_topics, num_publishers, num_subscribers, num_messages",
@pytest.mark.parametrize(
"num_topics, num_publishers, num_subscribers, num_messages",
[
(connector_cls, 3, 10, 1, 100),
(connector_cls, 3, 10, 10, 100),
(3, 10, 1, 100),
(3, 10, 10, 100),
],
)
async def test_pubsub_channel_multiple_topics_and_publishers(
Expand Down
24 changes: 14 additions & 10 deletions tests/integration/test_process_stop_cancel.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
import typing as _t

import pytest
import pytest_cases

from plugboard.component import Component, IOController as IO
from plugboard.connector import (
Expand All @@ -16,11 +15,12 @@
ConnectorBuilder,
RabbitMQConnector,
RayConnector,
ZMQConnector,
)
from plugboard.events import StopEvent
from plugboard.process import LocalProcess, Process, RayProcess
from plugboard.schemas import ConnectorSpec, Status
from tests.conftest import ComponentTestHelper, zmq_connector_cls
from tests.conftest import ComponentTestHelper, ConnectorCase, connector_case_id


STOP_TOLERANCE = 3
Expand Down Expand Up @@ -57,16 +57,20 @@ async def step(self) -> None:


@pytest.mark.asyncio
@pytest_cases.parametrize(
@pytest.mark.parametrize(
"process_cls, connector_cls",
[
(LocalProcess, AsyncioConnector),
(LocalProcess, zmq_connector_cls),
(LocalProcess, RabbitMQConnector),
# (RayProcess, RayConnector), # TODO : Pubsub/StopEvent unsupported. See https://github.com/plugboard-dev/plugboard/issues/101.
(RayProcess, zmq_connector_cls),
(RayProcess, RabbitMQConnector),
(LocalProcess, ConnectorCase(AsyncioConnector)),
(LocalProcess, ConnectorCase(ZMQConnector, False)),
(LocalProcess, ConnectorCase(ZMQConnector, True)),
(LocalProcess, ConnectorCase(RabbitMQConnector)),
# (RayProcess, ConnectorCase(RayConnector)), # TODO : Pubsub/StopEvent unsupported. See https://github.com/plugboard-dev/plugboard/issues/101.
(RayProcess, ConnectorCase(ZMQConnector, False)),
(RayProcess, ConnectorCase(ZMQConnector, True)),
(RayProcess, ConnectorCase(RabbitMQConnector)),
],
indirect=["connector_cls"],
ids=connector_case_id,
)
async def test_process_stop_event(
process_cls: type[Process], connector_cls: type[Connector], ray_ctx: None
Expand Down Expand Up @@ -137,7 +141,7 @@ async def stop_after() -> None:


@pytest.mark.asyncio
@pytest_cases.parametrize(
@pytest.mark.parametrize(
"process_cls, connector_cls",
[
(LocalProcess, AsyncioConnector),
Expand Down
Loading
Loading