generated from mononoki/pyproj
[+] mvp1
basic working mvp
This commit is contained in:
190
tests/conftest.py
Normal file
190
tests/conftest.py
Normal file
@@ -0,0 +1,190 @@
|
||||
import asyncio
|
||||
from copy import deepcopy
|
||||
import importlib
|
||||
import importlib.util
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def client_module():
|
||||
"""Загружает общий тестовый клиент без запуска его командного интерфейса."""
|
||||
path = Path(__file__).parents[1] / "src" / ".test" / "client_test.py"
|
||||
spec = importlib.util.spec_from_file_location("artex_test_client", path)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
class Clock:
|
||||
"""Монотонные часы, время которых продвигает сам тест."""
|
||||
|
||||
def __init__(self):
|
||||
self.value = 1000.0
|
||||
|
||||
def __call__(self):
|
||||
return self.value
|
||||
|
||||
def advance(self, seconds):
|
||||
assert seconds >= 0
|
||||
self.value += seconds
|
||||
|
||||
|
||||
class SleepRequest:
|
||||
def __init__(self, delay):
|
||||
self.delay = delay
|
||||
self.task = asyncio.current_task()
|
||||
self.future = asyncio.get_running_loop().create_future()
|
||||
|
||||
def release(self):
|
||||
if not self.future.done():
|
||||
self.future.set_result(None)
|
||||
|
||||
|
||||
class AsyncioProxy:
|
||||
"""Подменяет только ожидание времени в выбранном серверном модуле."""
|
||||
|
||||
def __init__(self, timing, label):
|
||||
self.timing = timing
|
||||
self.label = label
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(asyncio, name)
|
||||
|
||||
def create_task(self, coroutine, **kwargs):
|
||||
return self.timing.spawn(coroutine, **kwargs)
|
||||
|
||||
async def sleep(self, delay, result=None):
|
||||
request = SleepRequest(delay)
|
||||
self.timing.queues[self.label].put_nowait(request)
|
||||
await request.future
|
||||
return result
|
||||
|
||||
|
||||
class Timing:
|
||||
def __init__(self):
|
||||
self.clock = Clock()
|
||||
self.tasks = []
|
||||
self.queues = {}
|
||||
self.waiting = {}
|
||||
|
||||
def install(self, module, monkeypatch, label):
|
||||
self.queues[label] = asyncio.Queue()
|
||||
self.waiting[label] = []
|
||||
monkeypatch.setattr(module, "asyncio", AsyncioProxy(self, label))
|
||||
# Do not invent a missing clock binding: that must remain a real error.
|
||||
for name in ("now", "monotonic"):
|
||||
if hasattr(module, name):
|
||||
monkeypatch.setattr(module, name, self.clock)
|
||||
|
||||
def spawn(self, coroutine, **kwargs):
|
||||
task = asyncio.create_task(coroutine, **kwargs)
|
||||
self.tasks.append(task)
|
||||
return task
|
||||
|
||||
async def sleeping(self, label, task=None):
|
||||
async with asyncio.timeout(1):
|
||||
while True:
|
||||
for request in list(self.waiting[label]):
|
||||
if request.future.done():
|
||||
self.waiting[label].remove(request)
|
||||
elif task is None or request.task is task:
|
||||
self.waiting[label].remove(request)
|
||||
return request
|
||||
self.waiting[label].append(await self.queues[label].get())
|
||||
|
||||
async def __aenter__(self):
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type, exc, traceback):
|
||||
for task in self.tasks:
|
||||
if not task.done():
|
||||
task.cancel()
|
||||
results = await asyncio.gather(*self.tasks, return_exceptions=True)
|
||||
if exc_type is None:
|
||||
for result in results:
|
||||
if isinstance(result, BaseException) and not isinstance(result, asyncio.CancelledError):
|
||||
raise result
|
||||
|
||||
|
||||
class ConnectionRecorder:
|
||||
def __init__(self):
|
||||
self.sent = []
|
||||
self.close_count = 0
|
||||
|
||||
async def send(self, packet):
|
||||
self.sent.append(deepcopy(packet))
|
||||
|
||||
async def close(self):
|
||||
self.close_count += 1
|
||||
|
||||
|
||||
class RouterRecorder:
|
||||
"""Наблюдатель вызовов менеджера; сам Router проверяется отдельно."""
|
||||
|
||||
def __init__(self):
|
||||
self.routes = {}
|
||||
self.moves = []
|
||||
self.removed = []
|
||||
self.data = []
|
||||
|
||||
def add(self, uid):
|
||||
self.routes.setdefault(uid, set())
|
||||
|
||||
def move(self, old_uid, new_uid):
|
||||
self.moves.append((old_uid, new_uid))
|
||||
if old_uid in self.routes:
|
||||
self.routes[new_uid] = self.routes.pop(old_uid)
|
||||
|
||||
def remove(self, uid):
|
||||
self.removed.append(uid)
|
||||
self.routes.pop(uid, None)
|
||||
|
||||
async def process(self, uid, command):
|
||||
self.data.append((uid, command))
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def timing():
|
||||
"""Даёт управляемые часы и ожидания без реальных пауз в 5/60 секунд."""
|
||||
return Timing()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def session_code(timing, monkeypatch):
|
||||
"""Загружает настоящую Session, подменяя только часы и ожидания."""
|
||||
module = importlib.import_module("session.session")
|
||||
timing.install(module, monkeypatch, "session")
|
||||
return module
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def make_session(session_code):
|
||||
"""Создаёт настоящую сессию с записью отправок и timeout-событий."""
|
||||
def create(uid=11):
|
||||
connection = ConnectionRecorder()
|
||||
events = []
|
||||
|
||||
async def on_timeout(source, event):
|
||||
events.append((source, deepcopy(event)))
|
||||
|
||||
session = session_code.Session(uid, connection, on_timeout)
|
||||
return SimpleNamespace(session=session, connection=connection, events=events)
|
||||
|
||||
return create
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def make_manager(session_code, timing, monkeypatch):
|
||||
"""Создаёт настоящий менеджер с наблюдателем маршрутов и управляемой очисткой."""
|
||||
module = importlib.import_module("session.manager")
|
||||
timing.install(module, monkeypatch, "manager")
|
||||
|
||||
def create():
|
||||
router = RouterRecorder()
|
||||
manager = module.SessionManager(router)
|
||||
return SimpleNamespace(manager=manager, router=router, connection=ConnectionRecorder)
|
||||
|
||||
return create
|
||||
Reference in New Issue
Block a user