generated from mononoki/pyproj
[+] mvp1
basic working mvp
This commit is contained in:
193
tests/test_session_manager.py
Normal file
193
tests/test_session_manager.py
Normal file
@@ -0,0 +1,193 @@
|
||||
"""События менеджера, окно восстановления и гонки с отложенной очисткой."""
|
||||
|
||||
import asyncio
|
||||
from copy import deepcopy
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
def run(scenario):
|
||||
asyncio.run(asyncio.wait_for(scenario(), 3))
|
||||
|
||||
|
||||
async def new_session(case, state="air"):
|
||||
connection = case.connection()
|
||||
uid = await case.manager.new(connection)
|
||||
case.manager[uid].state = state
|
||||
if state == "air":
|
||||
case.router.add(uid)
|
||||
return uid, connection
|
||||
|
||||
|
||||
async def finish_wait(request, allow_cancel=False):
|
||||
request.release()
|
||||
try:
|
||||
await asyncio.wait_for(request.task, 1)
|
||||
except asyncio.CancelledError:
|
||||
if not allow_cancel:
|
||||
raise
|
||||
|
||||
|
||||
def test_new_connections_have_distinct_live_uids(make_manager, timing):
|
||||
"""Проверяет разные UID одновременно существующих соединений."""
|
||||
async def scenario():
|
||||
async with timing:
|
||||
case = make_manager()
|
||||
first, _ = await new_session(case)
|
||||
second, _ = await new_session(case)
|
||||
assert first != second
|
||||
assert len(case.manager.sessions) == 2
|
||||
run(scenario)
|
||||
|
||||
|
||||
def test_connect_and_data_events_reach_router(make_manager, timing):
|
||||
"""Проверяет регистрацию connect и передачу UID/команды data роутеру."""
|
||||
async def scenario():
|
||||
async with timing:
|
||||
case = make_manager()
|
||||
uid, _ = await new_session(case, "ready")
|
||||
await case.manager.process(uid, {"type": "connect", "uid": uid})
|
||||
assert uid in case.router.routes
|
||||
await case.manager.process(uid, {"type": "data", "data": "telemetry"})
|
||||
assert case.router.data == [(uid, "telemetry")]
|
||||
run(scenario)
|
||||
|
||||
|
||||
def test_disconnect_event_stops_transport_and_retains_record(make_manager, timing):
|
||||
"""Проверяет немедленное закрытие транспорта по событию disconnect и начало окна восстановления."""
|
||||
async def scenario():
|
||||
async with timing:
|
||||
case = make_manager()
|
||||
uid, connection = await new_session(case)
|
||||
task = timing.spawn(case.manager.process(uid, {"type": "disconnect"}))
|
||||
await asyncio.sleep(0)
|
||||
assert connection.close_count == 1, "Менеджер не обработал событие disconnect"
|
||||
assert case.manager[uid].state == "stop"
|
||||
assert uid in case.manager.sessions
|
||||
request = await timing.sleeping("manager")
|
||||
assert request.delay == 60
|
||||
timing.clock.advance(60)
|
||||
await finish_wait(request)
|
||||
await asyncio.wait_for(task, 1)
|
||||
# If cleanup is detached, wait for its observable effect, not an arbitrary pause.
|
||||
async with asyncio.timeout(1):
|
||||
while uid in case.manager.sessions:
|
||||
await asyncio.sleep(0)
|
||||
assert uid not in case.router.routes
|
||||
run(scenario)
|
||||
|
||||
|
||||
def test_close_preserves_session_until_recovery_window_expires(make_manager, timing):
|
||||
"""Проверяет сохранение сессии на 60 секунд и удаление записи/маршрутов после окна."""
|
||||
async def scenario():
|
||||
async with timing:
|
||||
case = make_manager()
|
||||
uid, connection = await new_session(case)
|
||||
case.manager[uid].last_data = {"telemetry": {"battery": 87}}
|
||||
task = timing.spawn(case.manager.close(uid))
|
||||
request = await timing.sleeping("manager")
|
||||
assert request.delay == 60
|
||||
assert connection.close_count == 1
|
||||
assert case.manager[uid].state == "stop"
|
||||
assert case.manager[uid].last_data == {"telemetry": {"battery": 87}}
|
||||
assert uid in case.router.routes
|
||||
timing.clock.advance(60)
|
||||
await finish_wait(request)
|
||||
await task
|
||||
assert uid not in case.manager.sessions
|
||||
assert uid not in case.router.routes
|
||||
run(scenario)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("reason", ["active", "wrong_upf", "missing", "migrated"])
|
||||
def test_ineligible_recovery_does_not_move_data(make_manager, timing, reason):
|
||||
"""Проверяет отказ восстановления активной, отсутствующей, перенесённой сессии и неверного upf."""
|
||||
async def scenario():
|
||||
async with timing:
|
||||
case = make_manager()
|
||||
old_uid, _ = await new_session(case)
|
||||
old = case.manager[old_uid]
|
||||
old.state = "air" if reason == "active" else ("migrated" if reason == "migrated" else "stop")
|
||||
old.last_data = {"telemetry": {"battery": 87}}
|
||||
new_uid, _ = await new_session(case, "ready")
|
||||
await case.manager.process(new_uid, {
|
||||
"type": "reconnect",
|
||||
"uid": -1 if reason == "missing" else old_uid,
|
||||
"upf": old.upf + 1 if reason == "wrong_upf" else old.upf,
|
||||
})
|
||||
assert case.router.moves == []
|
||||
assert case.manager[new_uid].last_data == {}
|
||||
assert old.last_data == {"telemetry": {"battery": 87}}
|
||||
assert case.manager[new_uid].state == "ready"
|
||||
run(scenario)
|
||||
|
||||
|
||||
def test_successful_recovery_preserves_data_and_is_consumed_once(make_manager, timing):
|
||||
"""Проверяет перенос динамических данных ровно один раз и сохранение собственного handshake."""
|
||||
async def scenario():
|
||||
async with timing:
|
||||
case = make_manager()
|
||||
old_uid, _ = await new_session(case)
|
||||
old = case.manager[old_uid]
|
||||
old.state = "stop"
|
||||
old.last_data = {"telemetry": {"battery": 87}}
|
||||
old.dynamic_meta = {"active_in": {"control"}, "active_out": {"telemetry"}}
|
||||
expected_data = deepcopy(old.last_data)
|
||||
expected_meta = deepcopy(old.dynamic_meta)
|
||||
new_uid, _ = await new_session(case, "ready")
|
||||
event = {"type": "reconnect", "uid": old_uid, "upf": old.upf}
|
||||
await case.manager.process(new_uid, event)
|
||||
assert case.manager[new_uid].last_data == expected_data
|
||||
assert case.manager[new_uid].dynamic_meta == expected_meta
|
||||
assert case.manager[new_uid].state == "ready"
|
||||
assert case.router.moves == [(old_uid, new_uid)]
|
||||
|
||||
repeated_uid, _ = await new_session(case, "ready")
|
||||
await case.manager.process(repeated_uid, event)
|
||||
assert case.manager[repeated_uid].last_data == {}
|
||||
assert case.router.moves == [(old_uid, new_uid)]
|
||||
assert case.manager[new_uid].last_data == expected_data
|
||||
run(scenario)
|
||||
|
||||
|
||||
def test_expired_session_cannot_be_recovered(make_manager, timing):
|
||||
"""Проверяет отказ восстановления после удаления старой сессии по 60-секундному таймеру."""
|
||||
async def scenario():
|
||||
async with timing:
|
||||
case = make_manager()
|
||||
old_uid, _ = await new_session(case)
|
||||
old_upf = case.manager[old_uid].upf
|
||||
task = timing.spawn(case.manager.close(old_uid))
|
||||
request = await timing.sleeping("manager")
|
||||
timing.clock.advance(60)
|
||||
await finish_wait(request)
|
||||
await task
|
||||
new_uid, _ = await new_session(case, "ready")
|
||||
await case.manager.process(new_uid, {"type": "reconnect", "uid": old_uid, "upf": old_upf})
|
||||
assert case.router.moves == []
|
||||
assert case.manager[new_uid].last_data == {}
|
||||
run(scenario)
|
||||
|
||||
|
||||
def test_old_cleanup_cannot_delete_recovered_session(make_manager, timing):
|
||||
"""Проверяет, что старый таймер после восстановления не удаляет новую запись и её данные."""
|
||||
async def scenario():
|
||||
async with timing:
|
||||
case = make_manager()
|
||||
old_uid, _ = await new_session(case)
|
||||
old = case.manager[old_uid]
|
||||
old.last_data = {"telemetry": {"battery": 87}}
|
||||
cleanup = timing.spawn(case.manager.close(old_uid))
|
||||
request = await timing.sleeping("manager")
|
||||
new_uid, _ = await new_session(case, "ready")
|
||||
restored = case.manager[new_uid]
|
||||
await case.manager.process(new_uid, {"type": "reconnect", "uid": old_uid, "upf": old.upf})
|
||||
assert restored.last_data == {"telemetry": {"battery": 87}}
|
||||
timing.clock.advance(60)
|
||||
await finish_wait(request, allow_cancel=True)
|
||||
if not cleanup.cancelled():
|
||||
await cleanup
|
||||
assert case.manager[new_uid] is restored
|
||||
assert restored.last_data == {"telemetry": {"battery": 87}}
|
||||
assert new_uid in case.router.routes
|
||||
run(scenario)
|
||||
Reference in New Issue
Block a user