tests/core/test_event_hub.py¶
Source from this local checkout, regenerated when the reader rebuilds.
Line links use #L<number>; a GitHub line range opens its first line.
1 # Copyright 2025 Softwell S.r.l.2 #3 # Licensed under the Apache License, Version 2.0 (the "License");4 # you may not use this file except in compliance with the License.5 # You may obtain a copy of the License at6 #7 # https://www.apache.org/licenses/LICENSE-2.08 #9 # Unless required by applicable law or agreed to in writing, software10 # distributed under the License is distributed on an "AS IS" BASIS,11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.12 # See the License for the specific language governing permissions and13 # limitations under the License.14 15 """Tests for EventHub (core 1e Phase 3): in-memory per-session event fan-out.16 17 Real objects, no mocks: a live ``EventHub`` and real ``asyncio.Queue``s. The18 tests are ``async def`` so subscriptions bind to a running event loop, matching19 the hub's runtime (one server event loop).20 """21 22 from __future__ import annotations23 24 from genro_asgi.tasks import EventHub25 from genro_asgi.tasks.hub import QUEUE_MAXSIZE26 27 28 class TestSubscribePublish:29 """A subscriber receives every event published to its session."""30 31 async def test_publish_reaches_subscriber(self) -> None:32 hub = EventHub()33 queue = hub.subscribe("s1")34 hub.publish("s1", {"type": "progress", "value": 1})35 assert (await queue.get()) == {"type": "progress", "value": 1}36 37 async def test_two_subscribers_same_session_both_receive(self) -> None:38 hub = EventHub()39 q1 = hub.subscribe("s1")40 q2 = hub.subscribe("s1")41 hub.publish("s1", {"n": 7})42 assert (await q1.get()) == {"n": 7}43 assert (await q2.get()) == {"n": 7}44 45 async def test_publish_isolated_per_session(self) -> None:46 hub = EventHub()47 q1 = hub.subscribe("s1")48 hub.subscribe("s2")49 hub.publish("s1", {"only": "s1"})50 assert (await q1.get()) == {"only": "s1"}51 assert hub.subscribe("s2").empty() # a fresh s2 queue saw nothing52 53 54 class TestNoSubscriber:55 """Publishing to a session nobody watches is a silent no-op."""56 57 async def test_publish_without_subscriber_is_noop(self) -> None:58 hub = EventHub()59 hub.publish("ghost", {"lost": True}) # must not raise60 queue = hub.subscribe("ghost")61 assert queue.empty() # the earlier event is not replayed62 63 64 class TestBackpressure:65 """A full queue drops the OLDEST event (progress is a snapshot)."""66 67 async def test_full_queue_drops_oldest(self) -> None:68 hub = EventHub()69 queue = hub.subscribe("s1")70 for i in range(QUEUE_MAXSIZE + 3):71 hub.publish("s1", {"i": i})72 assert queue.qsize() == QUEUE_MAXSIZE73 first = await queue.get()74 assert first == {"i": 3} # 0,1,2 dropped as oldest75 76 77 class TestUnsubscribe:78 """Unsubscribing removes the queue and cleans up the empty session."""79 80 async def test_unsubscribe_stops_delivery(self) -> None:81 hub = EventHub()82 queue = hub.subscribe("s1")83 hub.unsubscribe("s1", queue)84 hub.publish("s1", {"after": "unsub"})85 assert queue.empty()86 87 async def test_unsubscribe_unknown_session_is_noop(self) -> None:88 hub = EventHub()89 queue = hub.subscribe("s1")90 hub.unsubscribe("nope", queue) # must not raise91 92 async def test_last_unsubscribe_drops_the_session(self) -> None:93 hub = EventHub()94 queue = hub.subscribe("s1")95 hub.unsubscribe("s1", queue)96 # a fresh subscribe rebuilds a distinct empty queue for the session97 again = hub.subscribe("s1")98 assert again is not queue99 assert again.empty()