1414
1515from __future__ import annotations
1616
17- import gc
1817import subprocess
1918import sys
2019
2120import pytest
2221
2322from livekit .rtc ._ffi_client import FfiClient , FfiHandle
24- from livekit .rtc ._proto import audio_frame_pb2 , ffi_pb2
25-
26-
27- def _allocate_real_handle () -> int :
28- req = ffi_pb2 .FfiRequest ()
29- req .new_audio_source .type = audio_frame_pb2 .AUDIO_SOURCE_NATIVE
30- req .new_audio_source .sample_rate = 48000
31- req .new_audio_source .num_channels = 1
32- req .new_audio_source .queue_size_ms = 100
33- resp = FfiClient .instance .request (req )
34- handle_id = resp .new_audio_source .source .handle .id
35- assert handle_id != 0
36- return int (handle_id )
3723
3824
3925def test_active_runtime_invalid_handle_asserts () -> None :
@@ -42,26 +28,57 @@ def test_active_runtime_invalid_handle_asserts() -> None:
4228 FfiHandle (999999 ).dispose ()
4329
4430
45- def test_live_handles_marked_consumed_and_no_assert_after_dispose () -> None :
46- handle_id = _allocate_real_handle ()
47- handle = FfiHandle (handle_id )
48- assert not handle .disposed
31+ # Runs in a fresh interpreter: FfiClient._dispose() tears down the process-wide
32+ # FFI runtime, and every later test in the same pytest process would then wait
33+ # forever for FFI callbacks that never arrive.
34+ _DISPOSE_MARKS_LIVE_HANDLES_CONSUMED = """
35+ import gc
36+
37+ from livekit.rtc._ffi_client import FfiClient, FfiHandle
38+ from livekit.rtc._proto import audio_frame_pb2, ffi_pb2
39+
40+ req = ffi_pb2.FfiRequest()
41+ req.new_audio_source.type = audio_frame_pb2.AUDIO_SOURCE_NATIVE
42+ req.new_audio_source.sample_rate = 48000
43+ req.new_audio_source.num_channels = 1
44+ req.new_audio_source.queue_size_ms = 100
45+ resp = FfiClient.instance.request(req)
46+ handle_id = resp.new_audio_source.source.handle.id
47+ assert handle_id != 0
48+
49+ handle = FfiHandle(handle_id)
50+ assert not handle.disposed
4951
50- # Global dispose marks all live handles consumed and sets _ffi_disposed
51- FfiClient .instance ._dispose ()
52- assert handle .disposed
52+ # Global dispose marks all live handles consumed and sets _ffi_disposed
53+ FfiClient.instance._dispose()
54+ assert handle.disposed
5355
54- # Explicit dispose or __del__ after global dispose should be a no-op and not assert
55- handle .dispose ()
56- handle .__del__ ()
56+ # Explicit dispose or __del__ after global dispose should be a no-op and not assert
57+ handle.dispose()
58+ handle.__del__()
59+
60+ # Newly instantiated handle after global dispose also safely no-ops without assert
61+ late_handle = FfiHandle(12345)
62+ late_handle.dispose()
63+
64+ del handle
65+ del late_handle
66+ gc.collect()
67+ print("ok")
68+ """
5769
58- # Newly instantiated handle after global dispose also safely no-ops without assert
59- late_handle = FfiHandle (12345 )
60- late_handle .dispose ()
6170
62- del handle
63- del late_handle
64- gc .collect ()
71+ def test_live_handles_marked_consumed_and_no_assert_after_dispose () -> None :
72+ result = subprocess .run (
73+ [sys .executable , "-c" , _DISPOSE_MARKS_LIVE_HANDLES_CONSUMED ],
74+ capture_output = True ,
75+ text = True ,
76+ timeout = 60 ,
77+ )
78+ assert result .returncode == 0 , result .stderr
79+ assert result .stdout .strip () == "ok"
80+ assert "AssertionError" not in result .stderr
81+ assert "Exception ignored" not in result .stderr
6582
6683
6784def test_subprocess_exit_with_undisposed_handle_has_no_assertion_error () -> None :
@@ -85,6 +102,7 @@ def test_subprocess_exit_with_undisposed_handle_has_no_assertion_error() -> None
85102 [sys .executable , "-c" , code ],
86103 capture_output = True ,
87104 text = True ,
105+ timeout = 60 ,
88106 )
89107 assert result .returncode == 0
90108 assert "AssertionError" not in result .stderr
0 commit comments