| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185 |
- """Tests for PulseAudio interface with injectable runner.
- Fixtures mirror the ACTUAL shape of `pactl -f json list sources|sinks`:
- a top-level JSON array, where monitor sources carry a non-empty
- ``monitor_source`` (the sink they mirror) and real inputs carry ``""``.
- """
- import json
- from stt_daemon.pulse import Device, PulseAudio
- REAL_SOURCES = [
- {
- "index": 12,
- "name": "alsa_output.pci-0000_05_00.1.hdmi-stereo.monitor",
- "description": "Monitor of Radeon HDMI",
- "monitor_source": "alsa_output.pci-0000_05_00.1.hdmi-stereo",
- },
- {
- "index": 15,
- "name": "alsa_input.usb-C-Media.mono-fallback",
- "description": "USB Audio Mono",
- "monitor_source": "",
- },
- ]
- REAL_SINKS = [
- {
- "index": 12,
- "name": "alsa_output.pci-0000_05_00.1.hdmi-stereo",
- "description": "Radeon HDMI",
- "monitor_source": "alsa_output.pci-0000_05_00.1.hdmi-stereo.monitor",
- },
- {
- "index": 13,
- "name": "alsa_output.usb-analog-stereo",
- "description": "USB Analog",
- "monitor_source": "alsa_output.usb-analog-stereo.monitor",
- },
- ]
- def _make_pa(sources=None, sinks=None):
- def fake_runner(args):
- if "list" in args and "sources" in args:
- return json.dumps(sources if sources is not None else [])
- if "list" in args and "sinks" in args:
- return json.dumps(sinks if sinks is not None else [])
- raise RuntimeError(f"Unexpected args: {args}")
- return PulseAudio(runner=fake_runner)
- def test_list_devices_excludes_monitors_and_uses_sink_monitor_source():
- """A .monitor source must NOT be an input; each sink contributes its monitor as output."""
- pa = _make_pa(REAL_SOURCES, REAL_SINKS)
- devices = pa.list_devices()
- inputs = [d for d in devices if d.kind == "input"]
- outputs = [d for d in devices if d.kind == "output"]
- assert [d.name for d in inputs] == ["alsa_input.usb-C-Media.mono-fallback"]
- assert [d.name for d in outputs] == [
- "alsa_output.pci-0000_05_00.1.hdmi-stereo.monitor",
- "alsa_output.usb-analog-stereo.monitor",
- ]
- assert outputs[0].description == "Radeon HDMI"
- def test_list_devices_exclude():
- """Excluded names (our own virtual devices) are omitted."""
- pa = _make_pa(REAL_SOURCES, REAL_SINKS)
- devices = pa.list_devices(
- exclude={
- "alsa_input.usb-C-Media.mono-fallback",
- "alsa_output.usb-analog-stereo.monitor",
- }
- )
- assert [d.name for d in devices] == [
- "alsa_output.pci-0000_05_00.1.hdmi-stereo.monitor"
- ]
- def test_list_sources_tolerates_dict_wrapped_output():
- """Parser must not crash on the legacy object-wrapped form."""
- def runner(args):
- return json.dumps(
- {"sources": [{"name": "in1", "description": "In 1", "monitor_source": ""}]}
- )
- pa = PulseAudio(runner=runner)
- assert [s["name"] for s in pa.list_sources()] == ["in1"]
- def test_sink_exists():
- """Test sink existence check."""
- pa = _make_pa(REAL_SOURCES, REAL_SINKS)
- assert pa.sink_exists("alsa_output.usb-analog-stereo")
- assert not pa.sink_exists("VirtualMicSink")
- def test_source_exists():
- """Test source existence check."""
- pa = _make_pa(REAL_SOURCES, REAL_SINKS)
- assert pa.source_exists("alsa_input.usb-C-Media.mono-fallback")
- assert pa.source_exists("alsa_output.pci-0000_05_00.1.hdmi-stereo.monitor")
- assert not pa.source_exists("nonexistent")
- def test_load_module_parses_id():
- """Test that load_module parses the numeric ID from pactl output."""
- def fake_runner(args):
- if "load-module" in args:
- return "42\n"
- raise RuntimeError(f"Unexpected args: {args}")
- pa = PulseAudio(runner=fake_runner)
- assert pa.load_module("module-null-sink", "sink_name=VirtualMicSink") == 42
- def test_unload_module_success():
- """Test unload_module returns True on success."""
- calls = []
- def fake_runner(args):
- if "unload-module" in args:
- calls.append(args)
- return ""
- raise RuntimeError(f"Unexpected args: {args}")
- pa = PulseAudio(runner=fake_runner)
- assert pa.unload_module(42) is True
- assert len(calls) == 1
- def test_unload_module_already_gone_real_message():
- """pactl reports a vanished module as 'Failure: No such entity'."""
- def fake_runner(args):
- if "unload-module" in args:
- raise RuntimeError("pactl failed: Failure: No such entity\n")
- raise RuntimeError(f"Unexpected args: {args}")
- pa = PulseAudio(runner=fake_runner)
- assert pa.unload_module(42) is False
- def test_short_modules_parsing():
- """Test that short_modules parses pactl list short modules output."""
- output = "0\tmodule-cli-protocol-tcp\n12\tmodule-null-sink\n13\tmodule-loopback\n"
- def fake_runner(args):
- if "short" in args and "modules" in args:
- return output
- raise RuntimeError(f"Unexpected args: {args}")
- pa = PulseAudio(runner=fake_runner)
- modules = pa.short_modules()
- assert modules == [
- (0, "module-cli-protocol-tcp"),
- (12, "module-null-sink"),
- (13, "module-loopback"),
- ]
- def test_default_sink_name():
- """Test default_sink_name returns the sink name."""
- def fake_runner(args):
- if "get-default-sink" in args:
- return "alsa_output.usb-analog-stereo\n"
- raise RuntimeError(f"Unexpected args: {args}")
- pa = PulseAudio(runner=fake_runner)
- assert pa.default_sink_name() == "alsa_output.usb-analog-stereo"
- def test_device_dataclass():
- """Test Device dataclass."""
- device = Device(name="alsa_input.usb-Foo", description="USB Input", kind="input")
- assert device.name == "alsa_input.usb-Foo"
- assert device.description == "USB Input"
- assert device.kind == "input"
|