Files
core/tests/conftest.py
FoxVoland c16ed7915a [+!] all but small
some small modifications, tests added
2026-10-07 22:42:46 +03:00

198 lines
6.2 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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] / "examples" / "client.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.expected_errors = []
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
def accept_task_error(self, task, error):
assert task in self.tasks and task.done() and not task.cancelled()
assert task.exception() is error
self.expected_errors.append(error)
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):
if not any(result is expected for expected in self.expected_errors):
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