test_bus.py 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  1. """Tests for the publish-only event bus and subscriber."""
  2. import os
  3. import shutil
  4. import socket
  5. import tempfile
  6. import threading
  7. import time
  8. import pytest
  9. from stt_daemon import events
  10. from stt_daemon.bus import EventPublisher, EventSubscriber
  11. @pytest.fixture
  12. def sock_path():
  13. # Short path: AF_UNIX paths are limited to ~108 bytes.
  14. directory = tempfile.mkdtemp(prefix="sttb-")
  15. try:
  16. yield os.path.join(directory, "s.sock")
  17. finally:
  18. shutil.rmtree(directory, ignore_errors=True)
  19. def _wait(predicate, timeout=3.0):
  20. deadline = time.time() + timeout
  21. while time.time() < deadline:
  22. if predicate():
  23. return True
  24. time.sleep(0.01)
  25. return predicate()
  26. class _Collector:
  27. def __init__(self, subscriber):
  28. self.events = []
  29. self._thread = threading.Thread(target=self._run, args=(subscriber,), daemon=True)
  30. self._thread.start()
  31. def _run(self, subscriber):
  32. try:
  33. for event in subscriber.events():
  34. self.events.append(event)
  35. except Exception: # noqa: BLE001 - collector must never crash the test thread
  36. pass
  37. def types(self):
  38. return [e["type"] for e in self.events]
  39. def wait_for(self, predicate, timeout=3.0):
  40. return _wait(lambda: predicate(self.events), timeout)
  41. def _has(event_list, type_):
  42. return any(e["type"] == type_ for e in event_list)
  43. def _count(event_list, type_):
  44. return sum(1 for e in event_list if e["type"] == type_)
  45. def test_publish_subscribe_in_order(sock_path):
  46. pub = EventPublisher(sock_path, ring_size=10)
  47. pub.start()
  48. sub = EventSubscriber(sock_path, reconnect_delay=0.05)
  49. collector = _Collector(sub)
  50. assert collector.wait_for(lambda e: _has(e, events.STATUS))
  51. for _ in range(5):
  52. pub.emit(events.SPEECH_START)
  53. assert collector.wait_for(lambda e: _count(e, events.SPEECH_START) == 5)
  54. seqs = [e["seq"] for e in collector.events if e["type"] == events.SPEECH_START]
  55. assert seqs == sorted(seqs)
  56. sub.close()
  57. pub.close()
  58. def test_emit_assigns_monotonic_seq(sock_path):
  59. pub = EventPublisher(sock_path, ring_size=10)
  60. pub.start()
  61. sub = EventSubscriber(sock_path, reconnect_delay=0.05)
  62. collector = _Collector(sub)
  63. assert collector.wait_for(lambda e: _has(e, events.STATUS))
  64. for _ in range(3):
  65. pub.emit(events.HEARTBEAT)
  66. assert collector.wait_for(lambda e: _count(e, events.HEARTBEAT) == 3)
  67. seqs = [e["seq"] for e in collector.events if e["type"] == events.HEARTBEAT]
  68. assert seqs == sorted(seqs)
  69. assert len(set(seqs)) == 3
  70. sub.close()
  71. pub.close()
  72. def test_ring_replay_on_attach(sock_path):
  73. pub = EventPublisher(sock_path, ring_size=10)
  74. pub.start()
  75. for i in range(3):
  76. pub.emit(events.TRANSCRIPT, text=f"t{i}")
  77. assert _wait(lambda: pub.ring_len() == 3)
  78. sub = EventSubscriber(sock_path, reconnect_delay=0.05)
  79. collector = _Collector(sub)
  80. assert collector.wait_for(lambda e: _has(e, events.RING_END))
  81. types = collector.types()
  82. assert types[0] == events.STATUS
  83. ring_end_index = types.index(events.RING_END)
  84. assert types[1:ring_end_index] == [events.TRANSCRIPT] * 3
  85. sub.close()
  86. pub.close()
  87. def test_two_subscribers_each_get_their_own_replay(sock_path):
  88. pub = EventPublisher(sock_path, ring_size=10)
  89. pub.start()
  90. pub.emit(events.TRANSCRIPT, text="a")
  91. assert _wait(lambda: pub.ring_len() == 1)
  92. subs = [EventSubscriber(sock_path, reconnect_delay=0.05) for _ in range(2)]
  93. collectors = [_Collector(s) for s in subs]
  94. for collector in collectors:
  95. assert collector.wait_for(lambda e: _has(e, events.RING_END))
  96. types = collector.types()
  97. assert types[0] == events.STATUS
  98. assert types[1:types.index(events.RING_END)] == [events.TRANSCRIPT]
  99. for sub in subs:
  100. sub.close()
  101. pub.close()
  102. def test_publisher_never_blocks_on_slow_subscriber(sock_path):
  103. pub = EventPublisher(sock_path, ring_size=50)
  104. pub.start()
  105. # A raw client that connects and never reads: its writer blocks in sendall.
  106. slow = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
  107. slow.connect(sock_path)
  108. assert _wait(lambda: pub.client_count() == 1)
  109. total = 20000
  110. start = time.perf_counter()
  111. for _ in range(total):
  112. pub.emit(events.HEARTBEAT, rms=0.01)
  113. elapsed = time.perf_counter() - start
  114. assert elapsed < 2.0, f"publish path blocked: {elapsed:.3f}s for {total} events"
  115. slow.close()
  116. assert _wait(lambda: pub.client_count() == 0)
  117. # The publisher is still healthy: a fresh subscriber attaches promptly.
  118. sub = EventSubscriber(sock_path, reconnect_delay=0.05)
  119. collector = _Collector(sub)
  120. assert collector.wait_for(lambda e: _has(e, events.RING_END))
  121. sub.close()
  122. pub.close()
  123. def test_subscriber_reconnects_after_restart(sock_path):
  124. pub = EventPublisher(sock_path, ring_size=10)
  125. pub.start()
  126. sub = EventSubscriber(sock_path, reconnect_delay=0.05)
  127. collector = _Collector(sub)
  128. assert collector.wait_for(lambda e: _has(e, events.STATUS))
  129. pub.emit(events.TRANSCRIPT, text="one")
  130. assert collector.wait_for(lambda e: _count(e, events.TRANSCRIPT) == 1)
  131. pub.close()
  132. pub2 = EventPublisher(sock_path, ring_size=10)
  133. pub2.start()
  134. assert collector.wait_for(lambda e: _count(e, events.STATUS) >= 2, timeout=5.0)
  135. pub2.emit(events.TRANSCRIPT, text="two")
  136. assert collector.wait_for(lambda e: _count(e, events.TRANSCRIPT) == 2, timeout=5.0)
  137. sub.close()
  138. pub2.close()
  139. def test_socket_is_private_and_removed_on_close(sock_path):
  140. pub = EventPublisher(sock_path, ring_size=10)
  141. pub.start()
  142. assert os.path.exists(sock_path)
  143. assert os.stat(sock_path).st_mode & 0o777 == 0o600
  144. pub.close()
  145. assert not os.path.exists(sock_path)
  146. def test_status_carries_provider_fields(sock_path):
  147. pub = EventPublisher(
  148. sock_path,
  149. ring_size=10,
  150. status_provider=lambda: {"capture": "VirtualMicSink.monitor", "segments_done": 7},
  151. )
  152. pub.start()
  153. sub = EventSubscriber(sock_path, reconnect_delay=0.05)
  154. collector = _Collector(sub)
  155. assert collector.wait_for(lambda e: _has(e, events.STATUS))
  156. status = next(e for e in collector.events if e["type"] == events.STATUS)
  157. assert status["capture"] == "VirtualMicSink.monitor"
  158. assert status["segments_done"] == 7
  159. assert "ring_seq" in status
  160. sub.close()
  161. pub.close()