"""Unit tests for mcbluetooth_esp32.event_queue.""" from __future__ import annotations import asyncio import pytest from mcbluetooth_esp32.event_queue import EventQueue from mcbluetooth_esp32.protocol import Event, MsgType # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- def _make_event(name: str, data: dict | None = None, ts: int | None = None) -> Event: return Event(type=MsgType.EVENT, event=name, data=data or {}, ts=ts) # --------------------------------------------------------------------------- # Tests: push / get_events basics # --------------------------------------------------------------------------- class TestPushAndGetEvents: """push() and get_events() without any async waiting.""" async def test_push_and_retrieve(self): q = EventQueue() q.push(_make_event("boot")) q.push(_make_event("connect")) events = q.get_events() assert len(events) == 2 assert events[0].event == "boot" assert events[1].event == "connect" async def test_get_events_filtered_by_name(self): q = EventQueue() q.push(_make_event("boot")) q.push(_make_event("connect")) q.push(_make_event("boot")) events = q.get_events(event_name="boot") assert len(events) == 2 assert all(e.event == "boot" for e in events) async def test_get_events_with_limit(self): q = EventQueue() for i in range(10): q.push(_make_event("tick", data={"i": i})) events = q.get_events(limit=3) assert len(events) == 3 # Should be the *most recent* three assert events[0].data["i"] == 7 assert events[1].data["i"] == 8 assert events[2].data["i"] == 9 async def test_get_events_with_since_ts(self): q = EventQueue() q.push(_make_event("a", ts=100)) q.push(_make_event("b", ts=200)) q.push(_make_event("c", ts=300)) events = q.get_events(since_ts=200) assert len(events) == 2 assert events[0].event == "b" assert events[1].event == "c" async def test_get_events_with_none_ts_excluded_by_since_ts(self): q = EventQueue() q.push(_make_event("a", ts=None)) q.push(_make_event("b", ts=500)) events = q.get_events(since_ts=100) assert len(events) == 1 assert events[0].event == "b" async def test_get_events_empty_queue(self): q = EventQueue() assert q.get_events() == [] async def test_len(self): q = EventQueue() assert len(q) == 0 q.push(_make_event("a")) assert len(q) == 1 q.push(_make_event("b")) assert len(q) == 2 # --------------------------------------------------------------------------- # Tests: wait_for # --------------------------------------------------------------------------- class TestWaitFor: """Async waiting for specific events.""" async def test_wait_for_resolves_on_push(self): q = EventQueue() async def _push_later(): await asyncio.sleep(0.05) q.push(_make_event("pair_complete", data={"addr": "AA:BB"})) task = asyncio.create_task(_push_later()) event = await q.wait_for(event_name="pair_complete", timeout=2.0) await task assert event.event == "pair_complete" assert event.data["addr"] == "AA:BB" async def test_wait_for_timeout(self): q = EventQueue() with pytest.raises(asyncio.TimeoutError): await q.wait_for(event_name="never_arrives", timeout=0.1) async def test_wait_for_finds_existing_event(self): """If a matching event is already in the history, return immediately.""" q = EventQueue() q.push(_make_event("boot", data={"v": "1.0"})) # Should return instantly without waiting event = await q.wait_for(event_name="boot", timeout=0.5) assert event.event == "boot" assert event.data["v"] == "1.0" async def test_wait_for_returns_most_recent_existing(self): """When scanning history, the most recent match is returned.""" q = EventQueue() q.push(_make_event("boot", data={"v": "old"})) q.push(_make_event("boot", data={"v": "new"})) event = await q.wait_for(event_name="boot", timeout=0.5) assert event.data["v"] == "new" async def test_wait_for_with_custom_match(self): q = EventQueue() async def _push_later(): await asyncio.sleep(0.05) q.push(_make_event("pair_complete", data={"success": False})) q.push(_make_event("pair_complete", data={"success": True})) task = asyncio.create_task(_push_later()) event = await q.wait_for( event_name="pair_complete", match=lambda e: e.data.get("success") is True, timeout=2.0, ) await task assert event.data["success"] is True async def test_wait_for_requires_at_least_one_filter(self): q = EventQueue() with pytest.raises(ValueError, match="at least one"): await q.wait_for(timeout=1.0) async def test_wait_for_with_match_only(self): """Using only a custom match function (no event_name).""" q = EventQueue() async def _push(): await asyncio.sleep(0.05) q.push(_make_event("x", data={"val": 42})) task = asyncio.create_task(_push()) event = await q.wait_for( match=lambda e: e.data.get("val") == 42, timeout=2.0, ) await task assert event.data["val"] == 42 # --------------------------------------------------------------------------- # Tests: clear # --------------------------------------------------------------------------- class TestClear: async def test_clear_removes_all_events(self): q = EventQueue() q.push(_make_event("a")) q.push(_make_event("b")) assert len(q) == 2 q.clear() assert len(q) == 0 assert q.get_events() == [] async def test_clear_does_not_cancel_waiters(self): """Waiters registered before clear() should still fire on new events.""" q = EventQueue() async def _push_after_clear(): await asyncio.sleep(0.05) q.clear() await asyncio.sleep(0.05) q.push(_make_event("target")) task = asyncio.create_task(_push_after_clear()) event = await q.wait_for(event_name="target", timeout=2.0) await task assert event.event == "target" # --------------------------------------------------------------------------- # Tests: max_events boundary # --------------------------------------------------------------------------- class TestMaxEvents: async def test_max_events_trims_oldest(self): q = EventQueue(max_events=10) for i in range(15): q.push(_make_event("tick", data={"i": i})) assert len(q) == 10 events = q.get_events() # Oldest five (0..4) should have been trimmed assert events[0].data["i"] == 5 assert events[-1].data["i"] == 14 async def test_max_events_exact_boundary(self): q = EventQueue(max_events=1000) for i in range(1001): q.push(_make_event("x", data={"i": i})) assert len(q) == 1000 events = q.get_events(limit=1000) assert events[0].data["i"] == 1 assert events[-1].data["i"] == 1000 # --------------------------------------------------------------------------- # Tests: multiple waiters # --------------------------------------------------------------------------- class TestMultipleWaiters: async def test_different_event_waiters(self): """Two waiters for different events should each get their own match.""" q = EventQueue() async def _push_events(): await asyncio.sleep(0.05) q.push(_make_event("alpha", data={"n": 1})) q.push(_make_event("beta", data={"n": 2})) task = asyncio.create_task(_push_events()) evt_a, evt_b = await asyncio.gather( q.wait_for(event_name="alpha", timeout=2.0), q.wait_for(event_name="beta", timeout=2.0), ) await task assert evt_a.event == "alpha" assert evt_a.data["n"] == 1 assert evt_b.event == "beta" assert evt_b.data["n"] == 2 async def test_same_event_wakes_all_matching_waiters(self): """If multiple waiters match the same event, they all get resolved.""" q = EventQueue() async def _push(): await asyncio.sleep(0.05) q.push(_make_event("boom")) task = asyncio.create_task(_push()) e1, e2 = await asyncio.gather( q.wait_for(event_name="boom", timeout=2.0), q.wait_for(event_name="boom", timeout=2.0), ) await task assert e1.event == "boom" assert e2.event == "boom"