generated from mononoki/pyproj
[+] mvp1
basic working mvp
This commit is contained in:
239
tests/test_session.py
Normal file
239
tests/test_session.py
Normal file
@@ -0,0 +1,239 @@
|
||||
"""Переходы протокола и жизненный цикл таймера отдельной сессии."""
|
||||
|
||||
import asyncio
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
def run(scenario):
|
||||
asyncio.run(asyncio.wait_for(scenario(), 3))
|
||||
|
||||
|
||||
async def tick(request, session):
|
||||
request.release()
|
||||
await asyncio.sleep(0)
|
||||
if session.watcher_task.done() and not session.watcher_task.cancelled():
|
||||
error = session.watcher_task.exception()
|
||||
if error is not None:
|
||||
raise error
|
||||
|
||||
|
||||
def test_initial_deadline_and_watcher(make_session, timing):
|
||||
"""Проверяет начальное окно в 60 секунд и запуск собственного воркера сессии."""
|
||||
async def scenario():
|
||||
async with timing:
|
||||
case = make_session()
|
||||
assert case.session.state == "new"
|
||||
assert case.session.timeout == timing.clock() + 60
|
||||
request = await timing.sleeping("session", case.session.watcher_task)
|
||||
assert request.delay > 0
|
||||
assert case.events == []
|
||||
run(scenario)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("state", ["new", "token", "ready", "air"])
|
||||
def test_expired_deadline_emits_one_disconnect(make_session, timing, state):
|
||||
"""Проверяет однократное timeout-событие во всех незавершённых и активном состояниях."""
|
||||
async def scenario():
|
||||
async with timing:
|
||||
case = make_session()
|
||||
case.session.state = state
|
||||
request = await timing.sleeping("session", case.session.watcher_task)
|
||||
timing.clock.advance(60)
|
||||
request.release()
|
||||
await asyncio.wait_for(case.session.watcher_task, 1)
|
||||
assert case.events == [(case.session.uid, {"type": "disconnect"})]
|
||||
run(scenario)
|
||||
|
||||
|
||||
def test_watcher_does_not_disconnect_before_deadline(make_session, timing):
|
||||
"""Проверяет, что воркер не отключает сессию раньше её дедлайна."""
|
||||
async def scenario():
|
||||
async with timing:
|
||||
case = make_session()
|
||||
request = await timing.sleeping("session", case.session.watcher_task)
|
||||
timing.clock.advance(59)
|
||||
await tick(request, case.session)
|
||||
assert case.events == []
|
||||
assert not case.session.watcher_task.done()
|
||||
await timing.sleeping("session", case.session.watcher_task)
|
||||
run(scenario)
|
||||
|
||||
|
||||
def test_heartbeat_extends_current_deadline(make_session, timing):
|
||||
"""Проверяет перенос дедлайна heartbeat и отсутствие отключения по старому сроку."""
|
||||
async def scenario():
|
||||
async with timing:
|
||||
case = make_session()
|
||||
case.session.state = "air"
|
||||
request = await timing.sleeping("session", case.session.watcher_task)
|
||||
timing.clock.advance(10)
|
||||
await case.session.feed({"command": "heartbeat_ask", "payload": {}})
|
||||
await asyncio.sleep(0)
|
||||
assert case.session.timeout == timing.clock() + 5
|
||||
assert case.connection.sent == [{"command": "heartbeat_answer"}]
|
||||
assert case.session.last_data == {}
|
||||
|
||||
timing.clock.advance(4)
|
||||
await case.session.feed({"command": "heartbeat_ask", "payload": {}})
|
||||
new_deadline = case.session.timeout
|
||||
timing.clock.advance(1)
|
||||
await tick(request, case.session)
|
||||
assert case.events == []
|
||||
request = await timing.sleeping("session", case.session.watcher_task)
|
||||
timing.clock.advance(new_deadline - timing.clock())
|
||||
request.release()
|
||||
await asyncio.wait_for(case.session.watcher_task, 1)
|
||||
assert case.events == [(case.session.uid, {"type": "disconnect"})]
|
||||
run(scenario)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("state", ["stop", "migrated"])
|
||||
def test_terminal_session_does_not_emit_timeout(make_session, timing, state):
|
||||
"""Проверяет завершение воркера без timeout для отключённой или перенесённой сессии."""
|
||||
async def scenario():
|
||||
async with timing:
|
||||
case = make_session()
|
||||
request = await timing.sleeping("session", case.session.watcher_task)
|
||||
case.session.state = state
|
||||
timing.clock.advance(100)
|
||||
request.release()
|
||||
await asyncio.wait_for(case.session.watcher_task, 1)
|
||||
assert case.events == []
|
||||
run(scenario)
|
||||
|
||||
|
||||
def test_stop_closes_transport_and_finishes_watcher(make_session, timing):
|
||||
"""Проверяет закрытие соединения, состояние stop и завершение таймерного воркера."""
|
||||
async def scenario():
|
||||
async with timing:
|
||||
case = make_session()
|
||||
request = await timing.sleeping("session", case.session.watcher_task)
|
||||
await case.session.stop()
|
||||
assert case.session.state == "stop"
|
||||
assert case.connection.close_count == 1
|
||||
request.release()
|
||||
await asyncio.wait_for(case.session.watcher_task, 1)
|
||||
assert case.events == []
|
||||
run(scenario)
|
||||
|
||||
|
||||
def test_connection_and_successful_handshake(make_session, timing):
|
||||
"""Проверяет new→token→ready→air, ответы сервера и метаданные портов."""
|
||||
async def scenario():
|
||||
async with timing:
|
||||
case = make_session()
|
||||
session = case.session
|
||||
assert await session.feed({"command": "connection", "payload": {
|
||||
"name": "rover", "type": "robot", "in": ["control"], "out": ["telemetry"],
|
||||
}}) is None
|
||||
assert session.state == "token"
|
||||
await asyncio.sleep(0)
|
||||
assert case.connection.sent == [{"command": "token_ask", "payload": session.uid, "upf": session.upf}]
|
||||
assert session.static_meta["name"] == "rover"
|
||||
assert session.static_meta["type"] == "robot"
|
||||
assert session.static_meta["in"] == {"control"}
|
||||
assert session.static_meta["out"] == {"telemetry"}
|
||||
assert session.dynamic_meta == {"active_in": set(), "active_out": set()}
|
||||
|
||||
assert await session.feed({"command": "token_answer", "payload": {"token": None, "upf": None}}) is None
|
||||
assert session.state == "ready"
|
||||
await asyncio.sleep(0)
|
||||
assert case.connection.sent[-1] == {"command": "handshake_ask"}
|
||||
assert await session.feed({"command": "handshake_answer", "payload": {"status": True, "state": None}}) == {
|
||||
"type": "connect", "uid": session.uid,
|
||||
}
|
||||
assert session.state == "air"
|
||||
run(scenario)
|
||||
|
||||
|
||||
def test_recovery_request_still_requires_own_handshake(make_session, timing):
|
||||
"""Проверяет нормализацию старого UID/upf и ожидание отдельного нового handshake."""
|
||||
async def scenario():
|
||||
async with timing:
|
||||
case = make_session()
|
||||
case.session.state = "token"
|
||||
event = await case.session.feed({"command": "token_answer", "payload": {"token": 123, "upf": 456}})
|
||||
assert event == {"type": "reconnect", "uid": 123, "upf": 456}
|
||||
assert case.session.state == "ready"
|
||||
event = await case.session.feed({"command": "handshake_answer", "payload": {"status": True}})
|
||||
assert event == {"type": "connect", "uid": case.session.uid}
|
||||
assert case.session.state == "air"
|
||||
run(scenario)
|
||||
|
||||
|
||||
def test_data_updates_latest_value_for_each_command(make_session, timing):
|
||||
"""Проверяет нормализованные data-события и замену последнего значения по команде."""
|
||||
async def scenario():
|
||||
async with timing:
|
||||
case = make_session()
|
||||
case.session.state = "air"
|
||||
for command, payload in [("telemetry", {"battery": 87}), ("status", {"ok": True}), ("telemetry", {"battery": 86})]:
|
||||
assert await case.session.feed({"command": command, "payload": payload}) == {"type": "data", "data": command}
|
||||
assert case.session.last_data == {"telemetry": {"battery": 86}, "status": {"ok": True}}
|
||||
run(scenario)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("state", ["new", "token", "ready"])
|
||||
def test_wrong_sequence_disconnects_at_error_limit(make_session, timing, state):
|
||||
"""Проверяет четыре допустимых логических ошибки и disconnect на пятой."""
|
||||
async def scenario():
|
||||
async with timing:
|
||||
case = make_session()
|
||||
case.session.state = state
|
||||
for _ in range(4):
|
||||
assert await case.session.feed({"command": "telemetry", "payload": {}}) is None
|
||||
assert await case.session.feed({"command": "telemetry", "payload": {}}) == {"type": "disconnect"}
|
||||
run(scenario)
|
||||
|
||||
|
||||
def test_missing_command_requests_disconnect(make_session, timing):
|
||||
"""Проверяет немедленное disconnect-событие у пакета без команды."""
|
||||
async def scenario():
|
||||
async with timing:
|
||||
case = make_session()
|
||||
assert await case.session.feed({"payload": {}}) == {"type": "disconnect"}
|
||||
run(scenario)
|
||||
|
||||
|
||||
def test_failed_handshake_waits_before_retry(make_session, timing):
|
||||
"""Проверяет реальное ожидание задержки перед повторным запросом handshake."""
|
||||
async def scenario():
|
||||
async with timing:
|
||||
case = make_session()
|
||||
case.session.state = "ready"
|
||||
task = timing.spawn(case.session.feed({"command": "handshake_answer", "payload": {"status": False, "state": "booting"}}))
|
||||
await asyncio.sleep(0)
|
||||
assert not task.done(), "Повторный handshake выполняется без ожидания задержки"
|
||||
request = await timing.sleeping("session", task)
|
||||
assert request.delay == 1
|
||||
assert case.connection.sent == []
|
||||
request.release()
|
||||
assert await task == {"type": "await handshake", "state": "booting"}
|
||||
await asyncio.sleep(0)
|
||||
assert case.connection.sent == [{"command": "handshake_ask"}]
|
||||
assert case.session.state == "ready"
|
||||
run(scenario)
|
||||
|
||||
|
||||
def test_handshake_retry_limit_requests_disconnect(make_session, timing):
|
||||
"""Проверяет отказ от дальнейших повторов после пяти неудачных retry handshake."""
|
||||
async def scenario():
|
||||
async with timing:
|
||||
case = make_session()
|
||||
case.session.state = "ready"
|
||||
case.session.timeout_attempt = 5
|
||||
assert await case.session.feed({"command": "handshake_answer", "payload": {"status": False}}) == {"type": "disconnect"}
|
||||
run(scenario)
|
||||
|
||||
|
||||
def test_stopped_session_does_not_send_packets(make_session, timing):
|
||||
"""Проверяет, что после stop новые отправки не поступают закрытому соединению."""
|
||||
async def scenario():
|
||||
async with timing:
|
||||
case = make_session()
|
||||
await case.session.stop()
|
||||
case.session.send({"command": "telemetry", "payload": {}})
|
||||
await asyncio.sleep(0)
|
||||
assert case.connection.sent == []
|
||||
run(scenario)
|
||||
Reference in New Issue
Block a user