test_audio.py 2.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116
  1. """Tests for audio capture backends using fakes (no hardware, no subprocesses)."""
  2. import queue
  3. import pytest
  4. from stt_daemon.audio import ParecCapture, SoundDeviceCapture
  5. from stt_daemon.config import AudioConfig
  6. class FakeStdout:
  7. def __init__(self, chunks):
  8. self._chunks = list(chunks)
  9. def read(self, _n):
  10. return self._chunks.pop(0) if self._chunks else b""
  11. class FakeProc:
  12. def __init__(self, chunks=()):
  13. self.stdout = FakeStdout(chunks)
  14. self.terminated = 0
  15. self.killed = 0
  16. self.waits = 0
  17. def terminate(self):
  18. self.terminated += 1
  19. def wait(self, timeout=None):
  20. self.waits += 1
  21. return 0
  22. def kill(self):
  23. self.killed += 1
  24. def poll(self):
  25. return None
  26. class FakeStream:
  27. def __init__(self):
  28. self.stopped = 0
  29. self.closed = 0
  30. self.active = True
  31. def stop(self):
  32. self.stopped += 1
  33. self.active = False
  34. def close(self):
  35. self.closed += 1
  36. def _parec(chunks=()):
  37. cap = ParecCapture.__new__(ParecCapture)
  38. cap.cfg = AudioConfig()
  39. cap._frame_bytes = 4
  40. cap._proc = FakeProc(chunks)
  41. return cap
  42. def _sounddevice():
  43. cap = SoundDeviceCapture.__new__(SoundDeviceCapture)
  44. cap.cfg = AudioConfig()
  45. cap._queue = queue.Queue()
  46. cap._read_timeout_s = 0.05
  47. cap._stream = FakeStream()
  48. return cap
  49. def test_parec_frames_yield_until_eof():
  50. cap = _parec(chunks=[b"abcd", b"efgh", b""])
  51. gen = cap.frames()
  52. assert next(gen) == b"abcd"
  53. assert next(gen) == b"efgh"
  54. with pytest.raises(RuntimeError, match="parec EOF"):
  55. next(gen)
  56. def test_parec_short_read_raises():
  57. cap = _parec(chunks=[b"ab"])
  58. with pytest.raises(RuntimeError, match="short read"):
  59. next(cap.frames())
  60. def test_parec_close_is_idempotent():
  61. cap = _parec()
  62. proc = cap._proc
  63. cap.close()
  64. cap.close()
  65. assert proc.terminated == 1
  66. assert proc.waits == 1
  67. assert cap.healthy() is False
  68. def test_sounddevice_frames_yield_queued_frames():
  69. cap = _sounddevice()
  70. cap._queue.put(b"frame1")
  71. gen = cap.frames()
  72. assert next(gen) == b"frame1"
  73. def test_sounddevice_stall_raises_instead_of_silent_exit():
  74. cap = _sounddevice()
  75. with pytest.raises(RuntimeError, match="stalled"):
  76. next(cap.frames())
  77. def test_sounddevice_close_is_idempotent():
  78. cap = _sounddevice()
  79. stream = cap._stream
  80. cap.close()
  81. cap.close()
  82. assert stream.stopped == 1
  83. assert stream.closed == 1
  84. assert cap.healthy() is False