80 lines
2.8 KiB
Python
80 lines
2.8 KiB
Python
import asyncio
|
|
from types import SimpleNamespace
|
|
from unittest.mock import AsyncMock
|
|
import pytest
|
|
|
|
from defense_sim.server import socketio_server as server
|
|
|
|
|
|
class FakeSimulation:
|
|
def __init__(self, run_id, steps=4):
|
|
self.run_id = run_id
|
|
self.projectiles = [SimpleNamespace(active=True)]
|
|
self.events = []
|
|
self.remaining = steps
|
|
self.time_s = 0.
|
|
self.dt = .001
|
|
|
|
def step(self):
|
|
base = len(self.events)
|
|
self.events.extend({"type": "probe", "index": base+i} for i in range(100))
|
|
self.remaining -= 1
|
|
self.time_s += self.dt
|
|
self.projectiles[0].active = self.remaining > 0
|
|
|
|
def snapshot(self):
|
|
return {"run_id": self.run_id, "events": self.events[-250:]}
|
|
|
|
|
|
def test_event_history_cursor_and_socket_isolation(monkeypatch):
|
|
emit = AsyncMock()
|
|
monkeypatch.setattr(server.sio, 'emit', emit)
|
|
|
|
async def run():
|
|
await asyncio.gather(server._publish_loop(FakeSimulation('a'), 'client-a'),
|
|
server._publish_loop(FakeSimulation('b'), 'client-b'))
|
|
asyncio.run(run())
|
|
for sid in ('client-a', 'client-b'):
|
|
events = [call.args[1] for call in emit.call_args_list
|
|
if call.args[0] == server.EVENT and call.kwargs['to'] == sid]
|
|
assert len(events) == 400
|
|
assert [event['index'] for event in events] == list(range(400))
|
|
assert all(call.kwargs['to'] in {'client-a', 'client-b'} for call in emit.call_args_list)
|
|
|
|
|
|
def test_cancel_only_requested_connection(monkeypatch):
|
|
async def run():
|
|
waiting = asyncio.Event()
|
|
a, b = asyncio.create_task(waiting.wait()), asyncio.create_task(waiting.wait())
|
|
monkeypatch.setattr(server, 'tasks', {'a': a, 'b': b})
|
|
await server._cancel_run('a')
|
|
assert a.cancelled() and not b.done()
|
|
await server._cancel_run('b')
|
|
asyncio.run(run())
|
|
|
|
|
|
def test_live_clock_never_runs_ahead_and_transport_cost_does_not_add_drift(monkeypatch):
|
|
clock = [100.]
|
|
sleeps = []
|
|
sim = FakeSimulation('paced', steps=8)
|
|
sim.dt = .1
|
|
sim.time_s = 7. # restarting a nonzero engine must establish a new origin
|
|
real_sleep = asyncio.sleep
|
|
monkeypatch.setattr(server.time, 'perf_counter', lambda: clock[0])
|
|
|
|
async def sleep(delay):
|
|
sleeps.append(delay)
|
|
clock[0] += delay
|
|
await real_sleep(0)
|
|
|
|
async def emit(event, payload, **kwargs):
|
|
assert sim.time_s-7. <= clock[0]-100.+1e-9
|
|
clock[0] += .0001 # network emission takes wall time
|
|
|
|
monkeypatch.setattr(server.asyncio, 'sleep', sleep)
|
|
monkeypatch.setattr(server.sio, 'emit', emit)
|
|
asyncio.run(server._publish_loop(sim, 'client'))
|
|
assert sim.time_s == pytest.approx(7.8)
|
|
assert .8 <= clock[0]-100. < .825
|
|
assert any(delay > 0 for delay in sleeps)
|