diff --git a/pyproject.toml b/pyproject.toml index 7b710568..bfc7fb00 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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", diff --git a/tests/conftest.py b/tests/conftest.py index 66e0858e..3f4c63fa 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -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 @@ -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} @@ -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): diff --git a/tests/integration/test_channel.py b/tests/integration/test_channel.py index c067591e..8eb9dd78 100644 --- a/tests/integration/test_channel.py +++ b/tests/integration/test_channel.py @@ -6,7 +6,6 @@ from plugboard_schemas.connector import ConnectorMode, ConnectorSpec import pytest -import pytest_cases from plugboard.connector import ( Connector, @@ -16,7 +15,7 @@ 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, @@ -24,35 +23,35 @@ ) -@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") diff --git a/tests/integration/test_component_decorator.py b/tests/integration/test_component_decorator.py index 87e29bef..d2f9cfc8 100644 --- a/tests/integration/test_component_decorator.py +++ b/tests/integration/test_component_decorator.py @@ -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 @@ -83,7 +82,7 @@ async def step(self) -> None: @pytest.mark.asyncio -@pytest_cases.parametrize( +@pytest.mark.parametrize( "process_cls, connector_cls", [ (LocalProcess, AsyncioConnector), @@ -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), diff --git a/tests/integration/test_component_event_handlers.py b/tests/integration/test_component_event_handlers.py index 1ba77fe5..d5b1fa94 100644 --- a/tests/integration/test_component_event_handlers.py +++ b/tests/integration/test_component_event_handlers.py @@ -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): @@ -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 diff --git a/tests/integration/test_connector_pubsub.py b/tests/integration/test_connector_pubsub.py index 3ce90d58..60a56106 100644 --- a/tests/integration/test_connector_pubsub.py +++ b/tests/integration/test_connector_pubsub.py @@ -3,7 +3,6 @@ import typing as _t import pytest -import pytest_cases from plugboard.connector import ( Connector, @@ -11,8 +10,7 @@ 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, @@ -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( @@ -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( @@ -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( diff --git a/tests/integration/test_process_stop_cancel.py b/tests/integration/test_process_stop_cancel.py index 239dad1d..1da25ff6 100644 --- a/tests/integration/test_process_stop_cancel.py +++ b/tests/integration/test_process_stop_cancel.py @@ -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 ( @@ -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 @@ -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 @@ -137,7 +141,7 @@ async def stop_after() -> None: @pytest.mark.asyncio -@pytest_cases.parametrize( +@pytest.mark.parametrize( "process_cls, connector_cls", [ (LocalProcess, AsyncioConnector), diff --git a/tests/integration/test_process_with_components_run.py b/tests/integration/test_process_with_components_run.py index ca599d1e..2bb5e0de 100644 --- a/tests/integration/test_process_with_components_run.py +++ b/tests/integration/test_process_with_components_run.py @@ -10,7 +10,6 @@ from aiofile import async_open from pydantic import BaseModel import pytest -import pytest_cases from plugboard.component import IOController as IO from plugboard.component.component import IO_READ_TIMEOUT_SECONDS @@ -20,13 +19,14 @@ ConnectorBuilder, RabbitMQConnector, RayConnector, + ZMQConnector, ) from plugboard.events import Event from plugboard.exceptions import ConstraintError, NotInitialisedError, ProcessStatusError from plugboard.library import FileWriter 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 class A(ComponentTestHelper): @@ -84,16 +84,20 @@ def tempfile_path() -> _t.Generator[Path, None, None]: @pytest.mark.asyncio -@pytest_cases.parametrize( +@pytest.mark.parametrize( "process_cls, connector_cls", [ - (LocalProcess, AsyncioConnector), - (LocalProcess, zmq_connector_cls), - (LocalProcess, RabbitMQConnector), - (RayProcess, RayConnector), - (RayProcess, zmq_connector_cls), - (RayProcess, RabbitMQConnector), + (LocalProcess, ConnectorCase(AsyncioConnector)), + (LocalProcess, ConnectorCase(ZMQConnector, False)), + (LocalProcess, ConnectorCase(ZMQConnector, True)), + (LocalProcess, ConnectorCase(RabbitMQConnector)), + (RayProcess, ConnectorCase(RayConnector)), + (RayProcess, ConnectorCase(ZMQConnector, False)), + (RayProcess, ConnectorCase(ZMQConnector, True)), + (RayProcess, ConnectorCase(RabbitMQConnector)), ], + indirect=["connector_cls"], + ids=connector_case_id, ) @pytest.mark.parametrize( "iters, factor", @@ -200,7 +204,7 @@ async def _status_check(self) -> None: @pytest.mark.asyncio -@pytest_cases.parametrize( +@pytest.mark.parametrize( "process_cls, connector_cls", [ (LocalProcess, AsyncioConnector), @@ -259,7 +263,7 @@ async def step(self) -> None: @pytest.mark.asyncio -@pytest_cases.parametrize( +@pytest.mark.parametrize( "process_cls, connector_cls", [ (LocalProcess, AsyncioConnector), @@ -420,7 +424,7 @@ async def handle_action(self, evt: ActionEvent) -> None: @pytest.mark.asyncio -@pytest_cases.parametrize( +@pytest.mark.parametrize( "process_cls, connector_cls", [ (LocalProcess, AsyncioConnector), @@ -659,7 +663,7 @@ async def handle_message(self, event: MessageEvent) -> None: @pytest.mark.asyncio -@pytest_cases.parametrize( +@pytest.mark.parametrize( "process_cls, connector_cls", [ (LocalProcess, AsyncioConnector), diff --git a/tests/integration/test_state_backend_multiprocess.py b/tests/integration/test_state_backend_multiprocess.py index 848adf9f..02bc45a4 100644 --- a/tests/integration/test_state_backend_multiprocess.py +++ b/tests/integration/test_state_backend_multiprocess.py @@ -4,7 +4,6 @@ import pytest import pytest_asyncio -import pytest_cases from ray.util.multiprocessing import Pool import uvloop @@ -185,7 +184,7 @@ async def _inner() -> None: assert state_data_conn["times_upserted"] == 2 -@pytest_cases.parametrize("setup_backend", [setup_SqliteStateBackend, setup_PostgresStateBackend]) +@pytest.mark.parametrize("setup_backend", [setup_SqliteStateBackend, setup_PostgresStateBackend]) @pytest.mark.asyncio async def test_no_process_found_errors( setup_backend: _t.Callable[[], _t.ContextManager[StateBackend]], diff --git a/tests/unit/test_channel.py b/tests/unit/test_channel.py index f7aa0bb1..810a20be 100644 --- a/tests/unit/test_channel.py +++ b/tests/unit/test_channel.py @@ -4,7 +4,6 @@ import typing as _t import pytest -import pytest_cases from ray.util.multiprocessing import Pool from that_depends import ContextScopes, container_context @@ -18,8 +17,7 @@ from plugboard.exceptions import ChannelClosedError from plugboard.schemas import ConnectorMode, ConnectorSpec from plugboard.utils.di import DI -from plugboard.utils.settings import Settings -from tests.conftest import override_settings +from tests.conftest import ConnectorCase, configured_connector, connector_case_id TEST_ITEMS = [ @@ -33,23 +31,18 @@ ] -@pytest_cases.fixture -@pytest_cases.parametrize(zmq_pubsub_proxy=[False]) -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", [AsyncioConnector, RayConnector, zmq_connector_cls]) -def connector_cls(_connector_cls: type[Connector]) -> type[Connector]: - """Fixture for `Connector` of various types.""" - return _connector_cls +@pytest.fixture( + params=[ + ConnectorCase(AsyncioConnector), + ConnectorCase(RayConnector), + ConnectorCase(ZMQConnector, False), + ], + 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 @@ -84,11 +77,14 @@ async def test_channel(connector_cls: type[Connector], ray_ctx: None, job_id_ctx assert send_channel.is_closed -@pytest_cases.fixture -@pytest_cases.parametrize("_connector_cls_mp", [RayConnector, zmq_connector_cls]) -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 +@pytest.fixture( + params=[ConnectorCase(RayConnector), ConnectorCase(ZMQConnector, False)], + ids=connector_case_id, +) +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.mark.asyncio diff --git a/tests/unit/test_conftest.py b/tests/unit/test_conftest.py index d61f5bb1..1c1a09d0 100644 --- a/tests/unit/test_conftest.py +++ b/tests/unit/test_conftest.py @@ -1,18 +1,20 @@ """Unit tests for the shared pytest configuration.""" +import asyncio import os import pytest import uvloop +from plugboard.connector import AsyncioConnector, ZMQConnector from plugboard.utils.di import DI from plugboard.utils.settings import Settings from tests import conftest -def test_pytest_asyncio_loop_factories_uses_uvloop() -> None: - """The shared pytest-asyncio hook should configure uvloop factories.""" - assert conftest.pytest_asyncio_loop_factories() == {"uvloop": uvloop.new_event_loop} +async def test_tests_run_on_uvloop() -> None: + """Tests should run on a uvloop event loop, supplied by the loop factory hook.""" + assert isinstance(asyncio.get_running_loop(), uvloop.Loop) @pytest.mark.parametrize("proxy_enabled", [False, True]) @@ -44,3 +46,45 @@ def exercise_override() -> None: assert DI.settings.resolve_sync() is original assert dict(os.environ) == environment + + +@pytest.mark.parametrize( + "case", + [ + conftest.ConnectorCase(AsyncioConnector), + conftest.ConnectorCase(ZMQConnector, False), + conftest.ConnectorCase(ZMQConnector, True), + ], + ids=conftest.connector_case_id, +) +@pytest.mark.parametrize("raise_error", [False, True]) +def test_configured_connector_restores_settings( + monkeypatch: pytest.MonkeyPatch, case: conftest.ConnectorCase, raise_error: bool +) -> None: + """Connector cases override only explicit flags and restore settings even on failure.""" + monkeypatch.setenv("PLUGBOARD_FLAGS_ZMQ_PUBSUB_PROXY", str(not case.zmq_pubsub_proxy)) + monkeypatch.setenv("PLUGBOARD_IO_READ_TIMEOUT", "5.0") + original = DI.settings.resolve_sync() + environment = dict(os.environ) + + def exercise_case() -> None: + with conftest.configured_connector(case) as connector_cls: + assert connector_cls is case.connector_cls + settings = DI.settings.resolve_sync() + if case.zmq_pubsub_proxy is None: + assert settings is original + else: + assert settings.flags.zmq_pubsub_proxy is case.zmq_pubsub_proxy + assert settings.io_read_timeout == 5.0 + assert dict(os.environ) == environment + if raise_error: + raise RuntimeError("simulated test failure") + + if raise_error: + with pytest.raises(RuntimeError, match="simulated test failure"): + exercise_case() + else: + exercise_case() + + assert DI.settings.resolve_sync() is original + assert dict(os.environ) == environment diff --git a/tests/unit/test_connector_pubsub.py b/tests/unit/test_connector_pubsub.py index 17858251..11b20dc1 100644 --- a/tests/unit/test_connector_pubsub.py +++ b/tests/unit/test_connector_pubsub.py @@ -9,7 +9,6 @@ import typing as _t import pytest -import pytest_cases from plugboard.connector import ( AsyncioConnector, @@ -19,27 +18,17 @@ ) from plugboard.exceptions import ChannelClosedError from plugboard.schemas import ConnectorMode, ConnectorSpec -from plugboard.utils.settings import Settings -from tests.conftest import override_settings +from tests.conftest import ConnectorCase, configured_connector, connector_case_id -@pytest_cases.fixture -@pytest_cases.parametrize(zmq_pubsub_proxy=[False]) -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=[AsyncioConnector, zmq_connector_cls]) -def connector_cls(_connector_cls: type[Connector]) -> type[Connector]: - """Fixture for `Connector` of various types.""" - return _connector_cls +@pytest.fixture( + params=[ConnectorCase(AsyncioConnector), ConnectorCase(ZMQConnector, False)], + 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 TEST_ITEMS = string.ascii_lowercase @@ -133,11 +122,11 @@ async def recv_messages_unordered(channels: list[Channel]) -> list[int]: @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( @@ -188,11 +177,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( @@ -248,11 +237,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( diff --git a/uv.lock b/uv.lock index 55ab1c0f..a11b365c 100644 --- a/uv.lock +++ b/uv.lock @@ -1091,18 +1091,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e0/c3/7f67dea8ccf8fdcb9c99033bbe3e90b9e7395415843accb81428c441be2d/debugpy-1.8.20-py2.py3-none-any.whl", hash = "sha256:5be9bed9ae3be00665a06acaa48f8329d2b9632f15fd09f6a9a8c8d9907e54d7", size = 5337658, upload-time = "2026-01-29T23:04:17.404Z" }, ] -[[package]] -name = "decopatch" -version = "1.4.10" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "makefun" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/90/4c/8ca1f193428cbc4d63d0f07db9b8bd96be2db8ee5deefa93e7e8a28f2812/decopatch-1.4.10.tar.gz", hash = "sha256:957f49c93f4150182c23f8fb51d13bb3213e0f17a79e09c8cca7057598b55720", size = 69538, upload-time = "2022-03-01T08:57:21.79Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7b/fa/8e4a51e1afda8d4bd73d784bfe4a60cfdeeced9bea419eff5c271180377e/decopatch-1.4.10-py2.py3-none-any.whl", hash = "sha256:e151f7f93de2b1b3fd3f3272dcc7cefd1a69f68ec1c2d8e288ecd9deb36dc5f7", size = 18015, upload-time = "2022-03-01T08:57:20.676Z" }, -] - [[package]] name = "decorator" version = "5.2.1" @@ -2601,15 +2589,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0c/29/0348de65b8cc732daa3e33e67806420b2ae89bdce2b04af740289c5c6c8c/loguru-0.7.3-py3-none-any.whl", hash = "sha256:31a33c10c8e1e10422bfd431aeb5d351c7cf7fa671e3c4df004162264b28220c", size = 61595, upload-time = "2024-12-06T11:20:54.538Z" }, ] -[[package]] -name = "makefun" -version = "1.16.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7b/cf/6780ab8bc3b84a1cce3e4400aed3d64b6db7d5e227a2f75b6ded5674701a/makefun-1.16.0.tar.gz", hash = "sha256:e14601831570bff1f6d7e68828bcd30d2f5856f24bad5de0ccb22921ceebc947", size = 73565, upload-time = "2025-05-09T15:00:42.313Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/c0/4bc973defd1270b89ccaae04cef0d5fa3ea85b59b108ad2c08aeea9afb76/makefun-1.16.0-py2.py3-none-any.whl", hash = "sha256:43baa4c3e7ae2b17de9ceac20b669e9a67ceeadff31581007cca20a07bbe42c4", size = 22923, upload-time = "2025-05-09T15:00:41.042Z" }, -] - [[package]] name = "mako" version = "1.3.10" @@ -3853,7 +3832,6 @@ all = [ { name = "pytest" }, { name = "pytest-asyncio" }, { name = "pytest-benchmark" }, - { name = "pytest-cases" }, { name = "pytest-env" }, { name = "pytest-rerunfailures" }, { name = "radon" }, @@ -3900,7 +3878,6 @@ test = [ { name = "pytest" }, { name = "pytest-asyncio" }, { name = "pytest-benchmark" }, - { name = "pytest-cases" }, { name = "pytest-env" }, { name = "pytest-rerunfailures" }, { name = "ray", extra = ["default", "tune"] }, @@ -3967,10 +3944,9 @@ all = [ { name = "openai-responses", specifier = ">=0.11.4,<1" }, { name = "optuna", specifier = ">=3.0,<5" }, { name = "pre-commit", specifier = ">=3.8,<5" }, - { name = "pytest", specifier = ">=8.3,<9.1.0" }, - { name = "pytest-asyncio", specifier = ">=1.0,<1.4.0" }, + { name = "pytest", specifier = ">=8.3,<10" }, + { name = "pytest-asyncio", specifier = ">=1.4,<2" }, { name = "pytest-benchmark", specifier = ">=5.1.0" }, - { name = "pytest-cases", specifier = ">=3.8,<4" }, { name = "pytest-env", specifier = ">=1.1,<2" }, { name = "pytest-rerunfailures", specifier = ">=15.0,<17" }, { name = "radon", specifier = ">=6.0.1,<7" }, @@ -4014,10 +3990,9 @@ test = [ { name = "moto", extras = ["server"], specifier = ">=5.0,<6" }, { name = "openai-responses", specifier = ">=0.11.4,<1" }, { name = "optuna", specifier = ">=3.0,<5" }, - { name = "pytest", specifier = ">=8.3,<9.1.0" }, - { name = "pytest-asyncio", specifier = ">=1.0,<1.4.0" }, + { name = "pytest", specifier = ">=8.3,<10" }, + { name = "pytest-asyncio", specifier = ">=1.4,<2" }, { name = "pytest-benchmark", specifier = ">=5.1.0" }, - { name = "pytest-cases", specifier = ">=3.8,<4" }, { name = "pytest-env", specifier = ">=1.1,<2" }, { name = "pytest-rerunfailures", specifier = ">=15.0,<17" }, { name = "ray", extras = ["default", "tune"], specifier = ">=2.40.0,<3" }, @@ -4504,7 +4479,7 @@ wheels = [ [[package]] name = "pytest" -version = "9.0.3" +version = "9.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, @@ -4513,22 +4488,22 @@ dependencies = [ { name = "pluggy" }, { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, + { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, ] [[package]] name = "pytest-asyncio" -version = "1.3.0" +version = "1.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pytest" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/90/2c/8af215c0f776415f3590cac4f9086ccefd6fd463befeae41cd4d3f193e5a/pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5", size = 50087, upload-time = "2025-11-10T16:07:47.256Z" } +sdist = { url = "https://files.pythonhosted.org/packages/43/7c/d36d04db312ecf4298932ef77e6e4a9e8ad017906e24e34f0b0c361a2473/pytest_asyncio-1.4.0.tar.gz", hash = "sha256:c6c0d2259945122819f171a32ecea2c349ead889ee28176caaf492143424be42", size = 58514, upload-time = "2026-05-26T09:56:04.083Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5", size = 15075, upload-time = "2025-11-10T16:07:45.537Z" }, + { url = "https://files.pythonhosted.org/packages/03/e2/08a497ef684b88559c9cc5f4ad53a37e7b99e727094a86d6ea32536d5d3c/pytest_asyncio-1.4.0-py3-none-any.whl", hash = "sha256:933ca923a23075a87fb7070c0ec272a6848489824d887c85c812670932835aa1", size = 16930, upload-time = "2026-05-26T09:56:02.576Z" }, ] [[package]] @@ -4544,21 +4519,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/33/29/e756e715a48959f1c0045342088d7ca9762a2f509b945f362a316e9412b7/pytest_benchmark-5.2.3-py3-none-any.whl", hash = "sha256:bc839726ad20e99aaa0d11a127445457b4219bdb9e80a1afc4b51da7f96b0803", size = 45255, upload-time = "2025-11-09T18:48:39.765Z" }, ] -[[package]] -name = "pytest-cases" -version = "3.10.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "decopatch" }, - { name = "makefun" }, - { name = "packaging" }, - { name = "pytest" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c1/a2/c7abc3b606125cf3732e8e613092b0e2549365e824b5f37972125c22806b/pytest_cases-3.10.1.tar.gz", hash = "sha256:451f9e3ecd5d2d81a4362c10c441126f5b3d1ae3a7efaf59f60b1fb930df2d69", size = 1095867, upload-time = "2026-03-02T23:05:33.824Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/61/f2/7a29fb0571562034b05c38dceabba48dcc622be5d6c5448db80779e55de7/pytest_cases-3.10.1-py2.py3-none-any.whl", hash = "sha256:0deb8a85b6132e44adbc1cfc57897c6a624ec23f48ab445a43c7d56a6b9315a4", size = 108870, upload-time = "2026-03-02T23:05:32.663Z" }, -] - [[package]] name = "pytest-env" version = "1.7.0"