"""In-memory game store behaviour (the Redis store shares this interface).""" from __future__ import annotations import asyncio import unittest from tavolo.platform import GameSession, Seat from tavolo.platform.registry import GameRegistry from tavolo.platform.store import InMemoryGameStore from helpers import DummyEngine, async_test def _registry() -> GameRegistry: return GameRegistry([DummyEngine()]) def _session(game_id: str = "g1", code: str = "CODE01") -> GameSession: engine = DummyEngine() session = GameSession( id=game_id, game_type=engine.id, join_code=code, creator_sub="alice", players=[Seat(user_sub="alice", display_name="alice", team="A")], ) engine.create(session, {"target": 5}) return session class InMemoryGameStoreTest(unittest.TestCase): @async_test async def test_save_load_roundtrip(self) -> None: store = InMemoryGameStore(_registry()) session = _session() await store.save(session) loaded = await store.load("g1") self.assertIsNotNone(loaded) assert loaded is not None self.assertEqual("CODE01", loaded.join_code) self.assertEqual("dummy", loaded.game_type) self.assertEqual(5, loaded.state["target"]) self.assertEqual(["alice"], [p.user_sub for p in loaded.players]) # The loaded state is a deserialized copy, not the same object. self.assertIsNot(loaded.state, session.state) @async_test async def test_unknown_game_type_rejected(self) -> None: store = InMemoryGameStore(_registry()) session = _session() session.game_type = "nope" with self.assertRaises(Exception): await store.save(session) @async_test async def test_load_missing_returns_none(self) -> None: store = InMemoryGameStore(_registry()) self.assertIsNone(await store.load("nope")) self.assertIsNone(await store.find_by_code("NOPE01")) @async_test async def test_find_by_code(self) -> None: store = InMemoryGameStore(_registry()) await store.save(_session()) found = await store.find_by_code("code01") # case-insensitive self.assertIsNotNone(found) assert found is not None self.assertEqual("g1", found.id) @async_test async def test_load_returns_a_copy(self) -> None: store = InMemoryGameStore(_registry()) await store.save(_session()) first = await store.load("g1") assert first is not None first.state["target"] = 999 second = await store.load("g1") assert second is not None self.assertEqual(5, second.state["target"]) @async_test async def test_publish_reaches_subscriber(self) -> None: store = InMemoryGameStore(_registry()) await store.save(_session()) received = [] async with store.subscribe("g1") as events: await store.publish("g1") async for _ in events: received.append(True) break self.assertEqual([True], received) @async_test async def test_lock_serializes_concurrent_mutations(self) -> None: store = InMemoryGameStore(_registry()) order = [] async def holder() -> None: async with store.lock("g5"): order.append("holder-enter") await asyncio.sleep(0.05) order.append("holder-exit") async def contender() -> None: await asyncio.sleep(0.01) async with store.lock("g5"): order.append("contender") await asyncio.gather(holder(), contender()) self.assertEqual( ["holder-enter", "holder-exit", "contender"], order ) @async_test async def test_deadline_queue(self) -> None: store = InMemoryGameStore(_registry()) self.assertIsNone(await store.next_deadline()) self.assertEqual([], await store.due_deadlines(now=100.0)) await store.add_deadline("b", due_at=50.0) await store.add_deadline("a", due_at=10.0) await store.add_deadline("c", due_at=200.0) # Re-adding an existing member only updates its due time. await store.add_deadline("b", due_at=60.0) self.assertEqual(10.0, await store.next_deadline()) self.assertEqual(["a"], await store.due_deadlines(now=10.0)) self.assertEqual(["a", "b"], await store.due_deadlines(now=100.0)) # Due entries come out in due-time order and stay queued until removed. self.assertEqual(["a", "b"], await store.due_deadlines(now=100.0)) await store.remove_deadline("a") await store.remove_deadline("a") # removing twice is a no-op self.assertEqual(60.0, await store.next_deadline()) self.assertEqual(["b"], await store.due_deadlines(now=100.0)) if __name__ == "__main__": unittest.main()