Build 060.22: implement Acquisition Runtime Service
This commit is contained in:
@@ -0,0 +1,323 @@
|
||||
# app/tests/unit/market_data/acquisition/runtime/test_acquisition_runtime_service.py
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
|
||||
import pytest
|
||||
|
||||
from src.market_data.acquisition.runtime.acquisition_runtime_service import (
|
||||
AcquisitionRuntimeService,
|
||||
)
|
||||
from src.market_data.acquisition.runtime.acquisition_runtime_service_protocol import (
|
||||
AcquisitionRuntimeServiceProtocol,
|
||||
)
|
||||
from src.market_data.acquisition.runtime.runtime_commands import (
|
||||
ConnectCommand,
|
||||
DisconnectCommand,
|
||||
SendBinaryCommand,
|
||||
SendTextCommand,
|
||||
SubscribeCommand,
|
||||
UnsubscribeCommand,
|
||||
)
|
||||
from src.market_data.acquisition.runtime.transport_messages import (
|
||||
TransportBinaryMessage,
|
||||
TransportTextMessage,
|
||||
)
|
||||
from src.market_data.acquisition.runtime.websocket_protocol import (
|
||||
AcquisitionRuntimeCommandDispatcherProtocol,
|
||||
AcquisitionRuntimeEvent,
|
||||
AcquisitionSubscriptionMessage,
|
||||
)
|
||||
|
||||
|
||||
class FakeSession:
|
||||
def __init__(self) -> None:
|
||||
self.started = 0
|
||||
self.stopped = 0
|
||||
|
||||
@property
|
||||
def is_connected(self) -> bool:
|
||||
return False
|
||||
|
||||
async def start(self) -> None:
|
||||
self.started += 1
|
||||
|
||||
async def stop(self) -> None:
|
||||
self.stopped += 1
|
||||
|
||||
|
||||
class FakeTransport:
|
||||
def __init__(self) -> None:
|
||||
self.messages: list[str | bytes] = []
|
||||
|
||||
async def connect(self) -> None:
|
||||
return None
|
||||
|
||||
async def disconnect(self) -> None:
|
||||
return None
|
||||
|
||||
async def send(
|
||||
self,
|
||||
message: str | bytes,
|
||||
) -> None:
|
||||
self.messages.append(message)
|
||||
|
||||
async def receive(self) -> str | bytes:
|
||||
raise AssertionError("receive() should not be called.")
|
||||
|
||||
|
||||
class FakeSubscriptionManager:
|
||||
def __init__(self) -> None:
|
||||
self.subscriptions: list[
|
||||
tuple[str, AcquisitionSubscriptionMessage]
|
||||
] = []
|
||||
self.unsubscriptions: list[
|
||||
tuple[str, AcquisitionSubscriptionMessage]
|
||||
] = []
|
||||
|
||||
async def subscribe(
|
||||
self,
|
||||
subscription_key: str,
|
||||
message: AcquisitionSubscriptionMessage,
|
||||
) -> None:
|
||||
self.subscriptions.append(
|
||||
(
|
||||
subscription_key,
|
||||
message,
|
||||
)
|
||||
)
|
||||
|
||||
async def unsubscribe(
|
||||
self,
|
||||
subscription_key: str,
|
||||
message: AcquisitionSubscriptionMessage,
|
||||
) -> None:
|
||||
self.unsubscriptions.append(
|
||||
(
|
||||
subscription_key,
|
||||
message,
|
||||
)
|
||||
)
|
||||
|
||||
async def restore_subscriptions(self) -> None:
|
||||
return None
|
||||
|
||||
async def clear_subscriptions(self) -> None:
|
||||
return None
|
||||
|
||||
|
||||
class FakeEventPublisher:
|
||||
def __init__(self) -> None:
|
||||
self.events: list[AcquisitionRuntimeEvent] = []
|
||||
|
||||
async def publish(
|
||||
self,
|
||||
event: AcquisitionRuntimeEvent,
|
||||
) -> None:
|
||||
self.events.append(event)
|
||||
|
||||
|
||||
def create_service() -> tuple[
|
||||
AcquisitionRuntimeService,
|
||||
FakeSession,
|
||||
FakeTransport,
|
||||
FakeSubscriptionManager,
|
||||
FakeEventPublisher,
|
||||
]:
|
||||
session = FakeSession()
|
||||
transport = FakeTransport()
|
||||
subscriptions = FakeSubscriptionManager()
|
||||
publisher = FakeEventPublisher()
|
||||
|
||||
service = AcquisitionRuntimeService(
|
||||
session=session,
|
||||
transport=transport,
|
||||
subscription_manager=subscriptions,
|
||||
event_publisher=publisher,
|
||||
)
|
||||
|
||||
return (
|
||||
service,
|
||||
session,
|
||||
transport,
|
||||
subscriptions,
|
||||
publisher,
|
||||
)
|
||||
|
||||
|
||||
def test_service_implements_protocol() -> None:
|
||||
service, *_ = create_service()
|
||||
|
||||
assert isinstance(
|
||||
service,
|
||||
AcquisitionRuntimeServiceProtocol,
|
||||
)
|
||||
|
||||
|
||||
def test_service_is_command_dispatcher() -> None:
|
||||
service, *_ = create_service()
|
||||
|
||||
assert isinstance(
|
||||
service,
|
||||
AcquisitionRuntimeCommandDispatcherProtocol,
|
||||
)
|
||||
|
||||
|
||||
def test_dispatch_connect_command() -> None:
|
||||
service, session, *_ = create_service()
|
||||
|
||||
asyncio.run(
|
||||
service.dispatch(
|
||||
ConnectCommand(),
|
||||
)
|
||||
)
|
||||
|
||||
assert session.started == 1
|
||||
assert session.stopped == 0
|
||||
|
||||
|
||||
def test_dispatch_disconnect_command() -> None:
|
||||
service, session, *_ = create_service()
|
||||
|
||||
asyncio.run(
|
||||
service.dispatch(
|
||||
DisconnectCommand(),
|
||||
)
|
||||
)
|
||||
|
||||
assert session.started == 0
|
||||
assert session.stopped == 1
|
||||
|
||||
|
||||
def test_dispatch_subscribe_command() -> None:
|
||||
service, _, _, subscriptions, _ = create_service()
|
||||
|
||||
message = TransportTextMessage(
|
||||
payload='{"subscribe":true}',
|
||||
)
|
||||
|
||||
asyncio.run(
|
||||
service.dispatch(
|
||||
SubscribeCommand(
|
||||
subscription_key="BTCUSDT",
|
||||
message=message,
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
assert subscriptions.subscriptions == [
|
||||
(
|
||||
"BTCUSDT",
|
||||
message,
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
def test_dispatch_unsubscribe_command() -> None:
|
||||
service, _, _, subscriptions, _ = create_service()
|
||||
|
||||
message = TransportTextMessage(
|
||||
payload='{"unsubscribe":true}',
|
||||
)
|
||||
|
||||
asyncio.run(
|
||||
service.dispatch(
|
||||
UnsubscribeCommand(
|
||||
subscription_key="BTCUSDT",
|
||||
message=message,
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
assert subscriptions.unsubscriptions == [
|
||||
(
|
||||
"BTCUSDT",
|
||||
message,
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
def test_dispatch_send_text_command() -> None:
|
||||
service, _, transport, _, _ = create_service()
|
||||
|
||||
asyncio.run(
|
||||
service.dispatch(
|
||||
SendTextCommand(
|
||||
message=TransportTextMessage(
|
||||
payload="hello",
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
assert transport.messages == [
|
||||
"hello",
|
||||
]
|
||||
|
||||
|
||||
def test_dispatch_send_binary_command() -> None:
|
||||
service, _, transport, _, _ = create_service()
|
||||
|
||||
asyncio.run(
|
||||
service.dispatch(
|
||||
SendBinaryCommand(
|
||||
message=TransportBinaryMessage(
|
||||
payload=b"\x01\x02",
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
assert transport.messages == [
|
||||
b"\x01\x02",
|
||||
]
|
||||
|
||||
|
||||
def test_event_publisher_is_not_used_yet() -> None:
|
||||
service, _, _, _, publisher = create_service()
|
||||
|
||||
asyncio.run(
|
||||
service.dispatch(
|
||||
ConnectCommand(),
|
||||
)
|
||||
)
|
||||
|
||||
assert publisher.events == []
|
||||
|
||||
|
||||
def test_unsupported_command_raises_type_error() -> None:
|
||||
class UnsupportedCommand:
|
||||
pass
|
||||
|
||||
service, *_ = create_service()
|
||||
|
||||
with pytest.raises(
|
||||
TypeError,
|
||||
match="Неподдерживаемый тип Acquisition Runtime Command",
|
||||
):
|
||||
asyncio.run(
|
||||
service.dispatch(
|
||||
UnsupportedCommand(), # type: ignore[arg-type]
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def test_session_exception_is_propagated() -> None:
|
||||
class BrokenSession(FakeSession):
|
||||
async def start(self) -> None:
|
||||
raise RuntimeError("boom")
|
||||
|
||||
service = AcquisitionRuntimeService(
|
||||
session=BrokenSession(),
|
||||
transport=FakeTransport(),
|
||||
subscription_manager=FakeSubscriptionManager(),
|
||||
event_publisher=FakeEventPublisher(),
|
||||
)
|
||||
|
||||
with pytest.raises(RuntimeError, match="boom"):
|
||||
asyncio.run(
|
||||
service.dispatch(
|
||||
ConnectCommand(),
|
||||
)
|
||||
)
|
||||
@@ -7,6 +7,7 @@ from src.market_data.acquisition.runtime.websocket_protocol import (
|
||||
AcquisitionRuntimeCommandDispatcherProtocol,
|
||||
AcquisitionRuntimeEvent,
|
||||
AcquisitionRuntimeEventPublisherProtocol,
|
||||
AcquisitionSubscriptionMessage,
|
||||
WebSocketSessionProtocol,
|
||||
WebSocketSubscriptionManagerProtocol,
|
||||
WebSocketTransportProtocol,
|
||||
@@ -40,6 +41,20 @@ class FakeWebSocketSession:
|
||||
|
||||
|
||||
class FakeWebSocketSubscriptionManager:
|
||||
async def subscribe(
|
||||
self,
|
||||
subscription_key: str,
|
||||
message: AcquisitionSubscriptionMessage,
|
||||
) -> None:
|
||||
return None
|
||||
|
||||
async def unsubscribe(
|
||||
self,
|
||||
subscription_key: str,
|
||||
message: AcquisitionSubscriptionMessage,
|
||||
) -> None:
|
||||
return None
|
||||
|
||||
async def restore_subscriptions(self) -> None:
|
||||
return None
|
||||
|
||||
@@ -100,6 +115,20 @@ def test_incomplete_transport_does_not_satisfy_protocol() -> None:
|
||||
assert not isinstance(IncompleteTransport(), WebSocketTransportProtocol)
|
||||
|
||||
|
||||
def test_incomplete_subscription_manager_does_not_satisfy_protocol() -> None:
|
||||
class IncompleteSubscriptionManager:
|
||||
async def restore_subscriptions(self) -> None:
|
||||
return None
|
||||
|
||||
async def clear_subscriptions(self) -> None:
|
||||
return None
|
||||
|
||||
assert not isinstance(
|
||||
IncompleteSubscriptionManager(),
|
||||
WebSocketSubscriptionManagerProtocol,
|
||||
)
|
||||
|
||||
|
||||
def test_incomplete_command_dispatcher_does_not_satisfy_protocol() -> None:
|
||||
class IncompleteCommandDispatcher:
|
||||
pass
|
||||
@@ -117,4 +146,4 @@ def test_incomplete_event_publisher_does_not_satisfy_protocol() -> None:
|
||||
assert not isinstance(
|
||||
IncompleteEventPublisher(),
|
||||
AcquisitionRuntimeEventPublisherProtocol,
|
||||
)
|
||||
)
|
||||
Reference in New Issue
Block a user