basic working mvp
This commit is contained in:
2026-10-07 15:12:38 +03:00
parent 785a6980f6
commit fac12b2ea8
24 changed files with 1696 additions and 1 deletions

190
tests/conftest.py Normal file
View 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