test_picker.py 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219
  1. """Tests for interactive device picker (non-TTY fallback)."""
  2. import pytest
  3. from stt_daemon.picker import select_sources
  4. from stt_daemon.pulse import Device
  5. def test_select_sources_blank_keeps_preselected(monkeypatch, capsys):
  6. """Test that blank input keeps preselected sources."""
  7. devices = [
  8. Device(name="alsa_input.usb-Foo", description="USB Mic", kind="input"),
  9. Device(name="alsa_output.pci-0000.monitor", description="Speakers", kind="output"),
  10. ]
  11. monkeypatch.setattr("sys.stdin.isatty", lambda: False)
  12. monkeypatch.setattr("builtins.input", lambda _: "")
  13. result = select_sources(devices, preselected=["alsa_input.usb-Foo"])
  14. assert result == ["alsa_input.usb-Foo"]
  15. def test_select_sources_all(monkeypatch, capsys):
  16. """Test 'all' response."""
  17. devices = [
  18. Device(name="alsa_input.usb-Foo", description="USB Mic", kind="input"),
  19. Device(name="alsa_output.pci-0000.monitor", description="Speakers", kind="output"),
  20. ]
  21. monkeypatch.setattr("sys.stdin.isatty", lambda: False)
  22. monkeypatch.setattr("builtins.input", lambda _: "all")
  23. result = select_sources(devices, preselected=[])
  24. assert len(result) == 2
  25. assert "alsa_input.usb-Foo" in result
  26. assert "alsa_output.pci-0000.monitor" in result
  27. def test_select_sources_none(monkeypatch):
  28. """Test 'none' response."""
  29. devices = [
  30. Device(name="alsa_input.usb-Foo", description="USB Mic", kind="input"),
  31. Device(name="alsa_output.pci-0000.monitor", description="Speakers", kind="output"),
  32. ]
  33. monkeypatch.setattr("sys.stdin.isatty", lambda: False)
  34. monkeypatch.setattr("builtins.input", lambda _: "none")
  35. result = select_sources(devices, preselected=[])
  36. assert result == []
  37. def test_select_sources_space_separated_indices(monkeypatch):
  38. """Test space-separated numeric indices."""
  39. devices = [
  40. Device(name="alsa_input.usb-Foo", description="USB Mic", kind="input"),
  41. Device(name="alsa_output.pci-0000.monitor", description="Speakers", kind="output"),
  42. ]
  43. monkeypatch.setattr("sys.stdin.isatty", lambda: False)
  44. monkeypatch.setattr("builtins.input", lambda _: "1 2")
  45. result = select_sources(devices, preselected=[])
  46. assert len(result) == 2
  47. assert result[0] == "alsa_input.usb-Foo"
  48. assert result[1] == "alsa_output.pci-0000.monitor"
  49. def test_select_sources_comma_separated_indices(monkeypatch):
  50. """Test comma-separated numeric indices."""
  51. devices = [
  52. Device(name="alsa_input.usb-Foo", description="USB Mic", kind="input"),
  53. Device(name="alsa_output.pci-0000.monitor", description="Speakers", kind="output"),
  54. ]
  55. monkeypatch.setattr("sys.stdin.isatty", lambda: False)
  56. monkeypatch.setattr("builtins.input", lambda _: "2,1")
  57. result = select_sources(devices, preselected=[])
  58. # Order preserved from devices list (not selection order)
  59. assert len(result) == 2
  60. assert set(result) == {"alsa_input.usb-Foo", "alsa_output.pci-0000.monitor"}
  61. def test_select_sources_deduplicates(monkeypatch):
  62. """Test that duplicate indices are deduplicated."""
  63. devices = [
  64. Device(name="alsa_input.usb-Foo", description="USB Mic", kind="input"),
  65. Device(name="alsa_output.pci-0000.monitor", description="Speakers", kind="output"),
  66. ]
  67. monkeypatch.setattr("sys.stdin.isatty", lambda: False)
  68. monkeypatch.setattr("builtins.input", lambda _: "1 1 1")
  69. result = select_sources(devices, preselected=[])
  70. assert result == ["alsa_input.usb-Foo"]
  71. def test_select_sources_invalid_then_valid(monkeypatch):
  72. """Test recovery from invalid input."""
  73. devices = [
  74. Device(name="alsa_input.usb-Foo", description="USB Mic", kind="input"),
  75. Device(name="alsa_output.pci-0000.monitor", description="Speakers", kind="output"),
  76. ]
  77. inputs = iter(["invalid input", "all"])
  78. monkeypatch.setattr("sys.stdin.isatty", lambda: False)
  79. monkeypatch.setattr("builtins.input", lambda _: next(inputs))
  80. result = select_sources(devices, preselected=[])
  81. assert len(result) == 2
  82. def test_select_sources_eof_raises(monkeypatch):
  83. """Test that EOF raises RuntimeError with helpful message."""
  84. devices = [
  85. Device(name="alsa_input.usb-Foo", description="USB Mic", kind="input"),
  86. ]
  87. def raise_eof(_):
  88. raise EOFError()
  89. monkeypatch.setattr("sys.stdin.isatty", lambda: False)
  90. monkeypatch.setattr("builtins.input", raise_eof)
  91. with pytest.raises(RuntimeError, match="No TTY and EOF reached"):
  92. select_sources(devices, preselected=[])
  93. def test_select_sources_preserves_order(monkeypatch):
  94. """Test that result preserves device list order."""
  95. devices = [
  96. Device(name="device-a", description="A", kind="input"),
  97. Device(name="device-b", description="B", kind="input"),
  98. Device(name="device-c", description="C", kind="input"),
  99. ]
  100. # Select indices 3, 1, 2 but result should be in device order: 1, 2, 3
  101. monkeypatch.setattr("sys.stdin.isatty", lambda: False)
  102. monkeypatch.setattr("builtins.input", lambda _: "3 1 2")
  103. result = select_sources(devices, preselected=[])
  104. assert result == ["device-a", "device-b", "device-c"]
  105. def test_build_choices_preselects_via_checked():
  106. """Regression: questionary preselects via Choice.checked, NOT a default=[...] list.
  107. Passing a list as `default` to questionary.checkbox raises
  108. "Invalid `default` value passed ...", which broke `stt-daemon setup`.
  109. """
  110. pytest.importorskip("questionary")
  111. import questionary
  112. from stt_daemon.picker import _build_choices
  113. devices = [
  114. Device("alsa_input.a", "USB Mic", "input"),
  115. Device("alsa_output.b.monitor", "HDMI", "output"),
  116. ]
  117. choices = _build_choices(devices, {"alsa_output.b.monitor"})
  118. # Constructing the prompt must not raise.
  119. questionary.checkbox("Select audio sources to capture:", choices=choices)
  120. assert {c.value: c.checked for c in choices} == {
  121. "alsa_input.a": False,
  122. "alsa_output.b.monitor": True,
  123. }
  124. def test_select_sources_tui_path(monkeypatch):
  125. """TUI path returns the selection in device-list order."""
  126. pytest.importorskip("questionary")
  127. import questionary
  128. from stt_daemon import picker
  129. devices = [
  130. Device("b", "B", "input"),
  131. Device("a", "A", "input"),
  132. ]
  133. class FakePrompt:
  134. def ask(self):
  135. return ["a", "b"]
  136. monkeypatch.setattr("sys.stdin.isatty", lambda: True)
  137. monkeypatch.setattr(questionary, "checkbox", lambda *a, **k: FakePrompt())
  138. assert picker.select_sources(devices, preselected=[]) == ["b", "a"]
  139. def test_select_sources_tui_abort_keeps_preselected(monkeypatch):
  140. """Aborting the TUI (ask() returns None) preserves the previous selection."""
  141. pytest.importorskip("questionary")
  142. import questionary
  143. from stt_daemon import picker
  144. devices = [Device("a", "A", "input")]
  145. class AbortPrompt:
  146. def ask(self):
  147. return None
  148. monkeypatch.setattr("sys.stdin.isatty", lambda: True)
  149. monkeypatch.setattr(questionary, "checkbox", lambda *a, **k: AbortPrompt())
  150. assert picker.select_sources(devices, preselected=["a"]) == ["a"]