1212# See the License for the specific language governing permissions and
1313# limitations under the License.
1414
15- """Tests for FfiQueue event filtering functionality.
15+ """Tests for FfiQueue filter_fn functionality.
1616
17- These tests verify the event_types filtering feature of FfiQueue without
17+ These tests verify the filter_fn feature of FfiQueue without
1818requiring the native FFI library.
1919"""
2020
2121import asyncio
2222import threading
2323from dataclasses import dataclass
24- from typing import Generic , List , Optional , Set , TypeVar
24+ from typing import Callable , Generic , List , Optional , TypeVar
2525from unittest .mock import MagicMock
2626
2727import pytest
@@ -47,26 +47,23 @@ def empty(self) -> bool:
4747
4848
4949class FfiQueue (Generic [T ]):
50- """Copy of FfiQueue with event_types filtering for testing."""
50+ """Copy of FfiQueue with filter_fn for testing."""
5151
5252 def __init__ (self ) -> None :
5353 self ._lock = threading .RLock ()
5454 self ._subscribers : List [
55- tuple [Queue [T ], asyncio .AbstractEventLoop , Optional [Set [ str ]]]
55+ tuple [Queue [T ], asyncio .AbstractEventLoop , Optional [Callable [[ T ], bool ]]]
5656 ] = []
5757
5858 def put (self , item : T ) -> None :
59- which = None
60- try :
61- which = item .WhichOneof ("message" ) # type: ignore
62- except Exception :
63- pass
64-
6559 with self ._lock :
66- for queue , loop , event_types in self ._subscribers :
67- if event_types is not None and which is not None :
68- if which not in event_types :
69- continue
60+ for queue , loop , filter_fn in self ._subscribers :
61+ if filter_fn is not None :
62+ try :
63+ if not filter_fn (item ):
64+ continue
65+ except Exception :
66+ pass # On filter error, deliver the item
7067
7168 try :
7269 loop .call_soon_threadsafe (queue .put_nowait , item )
@@ -76,12 +73,12 @@ def put(self, item: T) -> None:
7673 def subscribe (
7774 self ,
7875 loop : Optional [asyncio .AbstractEventLoop ] = None ,
79- event_types : Optional [Set [ str ]] = None ,
76+ filter_fn : Optional [Callable [[ T ], bool ]] = None ,
8077 ) -> Queue [T ]:
8178 with self ._lock :
8279 queue = Queue [T ]()
8380 loop = loop or asyncio .get_event_loop ()
84- self ._subscribers .append ((queue , loop , event_types ))
81+ self ._subscribers .append ((queue , loop , filter_fn ))
8582 return queue
8683
8784 def unsubscribe (self , queue : Queue [T ]) -> None :
@@ -102,8 +99,8 @@ def WhichOneof(self, field: str) -> str:
10299 return self ._message_type
103100
104101
105- class TestFfiQueueEventFiltering :
106- """Test suite for FfiQueue event_types filtering ."""
102+ class TestFfiQueueFilterFn :
103+ """Test suite for FfiQueue filter_fn functionality ."""
107104
108105 @pytest .fixture
109106 def event_loop (self ):
@@ -113,11 +110,10 @@ def event_loop(self):
113110 loop .close ()
114111
115112 def test_subscribe_without_filter_receives_all_events (self , event_loop ):
116- """Subscriber without event_types filter receives all events."""
113+ """Subscriber without filter_fn receives all events."""
117114 queue = FfiQueue ()
118- sub = queue .subscribe (event_loop , event_types = None )
115+ sub = queue .subscribe (event_loop , filter_fn = None )
119116
120- # Send various event types
121117 events = [
122118 MockFfiEvent ("room_event" ),
123119 MockFfiEvent ("audio_stream_event" ),
@@ -128,22 +124,22 @@ def test_subscribe_without_filter_receives_all_events(self, event_loop):
128124 for event in events :
129125 queue .put (event )
130126
131- # Run event loop to process callbacks
132127 event_loop .run_until_complete (asyncio .sleep (0.01 ))
133128
134- # Should receive all 4 events
135129 received = []
136130 while not sub .empty ():
137131 received .append (sub .get_nowait ())
138132
139133 assert len (received ) == 4
140134
141135 def test_subscribe_with_filter_receives_only_matching_events (self , event_loop ):
142- """Subscriber with event_types filter only receives matching events."""
136+ """Subscriber with filter_fn only receives matching events."""
143137 queue = FfiQueue ()
144- sub = queue .subscribe (event_loop , event_types = {"audio_stream_event" })
138+ sub = queue .subscribe (
139+ event_loop ,
140+ filter_fn = lambda e : e .WhichOneof ("message" ) == "audio_stream_event" ,
141+ )
145142
146- # Send various event types
147143 events = [
148144 MockFfiEvent ("room_event" ),
149145 MockFfiEvent ("audio_stream_event" ),
@@ -155,10 +151,8 @@ def test_subscribe_with_filter_receives_only_matching_events(self, event_loop):
155151 for event in events :
156152 queue .put (event )
157153
158- # Run event loop to process callbacks
159154 event_loop .run_until_complete (asyncio .sleep (0.01 ))
160155
161- # Should receive only 2 audio_stream_events
162156 received = []
163157 while not sub .empty ():
164158 received .append (sub .get_nowait ())
@@ -170,16 +164,16 @@ def test_multiple_subscribers_different_filters(self, event_loop):
170164 """Multiple subscribers can have different filters."""
171165 queue = FfiQueue ()
172166
173- # Subscriber 1: only audio events
174- sub_audio = queue .subscribe (event_loop , event_types = {"audio_stream_event" })
175-
176- # Subscriber 2: only video events
177- sub_video = queue .subscribe (event_loop , event_types = {"video_stream_event" })
178-
179- # Subscriber 3: all events
180- sub_all = queue .subscribe (event_loop , event_types = None )
167+ sub_audio = queue .subscribe (
168+ event_loop ,
169+ filter_fn = lambda e : e .WhichOneof ("message" ) == "audio_stream_event" ,
170+ )
171+ sub_video = queue .subscribe (
172+ event_loop ,
173+ filter_fn = lambda e : e .WhichOneof ("message" ) == "video_stream_event" ,
174+ )
175+ sub_all = queue .subscribe (event_loop , filter_fn = None )
181176
182- # Send mixed events
183177 events = [
184178 MockFfiEvent ("room_event" ),
185179 MockFfiEvent ("audio_stream_event" ),
@@ -192,7 +186,6 @@ def test_multiple_subscribers_different_filters(self, event_loop):
192186
193187 event_loop .run_until_complete (asyncio .sleep (0.01 ))
194188
195- # Count received events
196189 audio_count = 0
197190 while not sub_audio .empty ():
198191 sub_audio .get_nowait ()
@@ -208,15 +201,17 @@ def test_multiple_subscribers_different_filters(self, event_loop):
208201 sub_all .get_nowait ()
209202 all_count += 1
210203
211- assert audio_count == 2 # 2 audio events
212- assert video_count == 1 # 1 video event
213- assert all_count == 4 # all 4 events
204+ assert audio_count == 2
205+ assert video_count == 1
206+ assert all_count == 4
214207
215208 def test_filter_with_multiple_event_types (self , event_loop ):
216- """Filter can accept multiple event types."""
209+ """Filter can match multiple event types."""
217210 queue = FfiQueue ()
218211 sub = queue .subscribe (
219- event_loop , event_types = {"audio_stream_event" , "video_stream_event" }
212+ event_loop ,
213+ filter_fn = lambda e : e .WhichOneof ("message" )
214+ in {"audio_stream_event" , "video_stream_event" },
220215 )
221216
222217 events = [
@@ -235,48 +230,46 @@ def test_filter_with_multiple_event_types(self, event_loop):
235230 while not sub .empty ():
236231 received .append (sub .get_nowait ())
237232
238- # Should receive audio and video events only
239233 assert len (received ) == 2
240234 types = {e ._message_type for e in received }
241235 assert types == {"audio_stream_event" , "video_stream_event" }
242236
243237 def test_unsubscribe_works_with_filtered_subscriber (self , event_loop ):
244238 """Unsubscribe correctly removes filtered subscriber."""
245239 queue = FfiQueue ()
246- sub = queue .subscribe (event_loop , event_types = {"audio_stream_event" })
240+ sub = queue .subscribe (
241+ event_loop ,
242+ filter_fn = lambda e : e .WhichOneof ("message" ) == "audio_stream_event" ,
243+ )
247244
248245 queue .put (MockFfiEvent ("audio_stream_event" ))
249246 event_loop .run_until_complete (asyncio .sleep (0.01 ))
250247
251- # Should have received 1 event
252248 assert not sub .empty ()
253249
254- # Unsubscribe
255250 queue .unsubscribe (sub )
256251
257- # Clear the queue
258252 while not sub .empty ():
259253 sub .get_nowait ()
260254
261- # Send more events
262255 queue .put (MockFfiEvent ("audio_stream_event" ))
263256 event_loop .run_until_complete (asyncio .sleep (0.01 ))
264257
265- # Should not receive after unsubscribe
266258 assert sub .empty ()
267259
268- def test_event_without_which_oneof_passes_through (self , event_loop ):
269- """Events without WhichOneof method pass through to all subscribers ."""
260+ def test_filter_error_delivers_item (self , event_loop ):
261+ """If filter_fn raises, item is still delivered ."""
270262 queue = FfiQueue ()
271- sub = queue .subscribe (event_loop , event_types = {"audio_stream_event" })
272263
273- # Event without WhichOneof
274- plain_event = MagicMock ( spec = []) # No WhichOneof method
264+ def bad_filter ( e ):
265+ raise ValueError ( "oops" )
275266
276- queue .put (plain_event )
267+ sub = queue .subscribe (event_loop , filter_fn = bad_filter )
268+
269+ queue .put (MockFfiEvent ("audio_stream_event" ))
277270 event_loop .run_until_complete (asyncio .sleep (0.01 ))
278271
279- # Should still receive it (can't filter without type info)
272+ # Item should be delivered despite filter error
280273 received = []
281274 while not sub .empty ():
282275 received .append (sub .get_nowait ())
@@ -300,18 +293,20 @@ def test_filtering_reduces_callback_calls(self, event_loop):
300293 # Create 10 subscribers, each only wants audio events
301294 subscribers = []
302295 for _ in range (10 ):
303- sub = queue .subscribe (event_loop , event_types = {"audio_stream_event" })
296+ sub = queue .subscribe (
297+ event_loop ,
298+ filter_fn = lambda e : e .WhichOneof ("message" ) == "audio_stream_event" ,
299+ )
304300 subscribers .append (sub )
305301
306302 # Generate 1000 events, only 5% are audio
307303 events = []
308304 for i in range (1000 ):
309- if i < 50 : # 5% audio events
305+ if i < 50 :
310306 events .append (MockFfiEvent ("audio_stream_event" ))
311307 else :
312308 events .append (MockFfiEvent ("room_event" ))
313309
314- # Process all events
315310 for event in events :
316311 queue .put (event )
317312
@@ -324,7 +319,3 @@ def test_filtering_reduces_callback_calls(self, event_loop):
324319 sub .get_nowait ()
325320 count += 1
326321 assert count == 50
327-
328- # Total callbacks made: 10 subscribers × 50 audio events = 500
329- # Without filtering: 10 subscribers × 1000 events = 10,000
330- # This is a 95% reduction in callback/object creation
0 commit comments