343240fec1
Nach dem ersten fehlgeschlagenen Bus-Connect wird der Controller dauerhaft deaktiviert, statt bei jedem Hotkey-Druck einen neuen Connect-Versuch zu starten. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
200 lines
5.8 KiB
Python
200 lines
5.8 KiB
Python
"""Tests für MprisController (Linux/MPRIS)."""
|
|
|
|
import sys
|
|
from unittest.mock import AsyncMock, MagicMock
|
|
|
|
import pytest
|
|
|
|
pytestmark = pytest.mark.skipif(sys.platform != "linux", reason="MPRIS is Linux-only")
|
|
|
|
|
|
def _make_player(status: str) -> MagicMock:
|
|
"""Erzeugt einen gemockten Player-Proxy mit gegebenem PlaybackStatus."""
|
|
player = MagicMock()
|
|
player.get_playback_status = AsyncMock(return_value=status)
|
|
player.call_pause = AsyncMock()
|
|
player.call_play = AsyncMock()
|
|
return player
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_pause_with_no_players_is_noop(monkeypatch):
|
|
from whisper_local.media._mpris import MprisController
|
|
|
|
controller = MprisController()
|
|
monkeypatch.setattr(
|
|
controller, "_list_player_names", AsyncMock(return_value=[])
|
|
)
|
|
monkeypatch.setattr(
|
|
controller, "_get_player_interface", AsyncMock()
|
|
)
|
|
|
|
await controller.pause()
|
|
|
|
assert controller._paused == []
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_pause_pauses_only_playing_players(monkeypatch):
|
|
from whisper_local.media._mpris import MprisController
|
|
|
|
playing = _make_player("Playing")
|
|
paused = _make_player("Paused")
|
|
|
|
async def fake_get_player(name: str):
|
|
return {"org.mpris.MediaPlayer2.spotify": playing,
|
|
"org.mpris.MediaPlayer2.vlc": paused}[name]
|
|
|
|
controller = MprisController()
|
|
monkeypatch.setattr(
|
|
controller,
|
|
"_list_player_names",
|
|
AsyncMock(return_value=[
|
|
"org.mpris.MediaPlayer2.spotify",
|
|
"org.mpris.MediaPlayer2.vlc",
|
|
]),
|
|
)
|
|
monkeypatch.setattr(controller, "_get_player_interface", fake_get_player)
|
|
|
|
await controller.pause()
|
|
|
|
playing.call_pause.assert_awaited_once()
|
|
paused.call_pause.assert_not_awaited()
|
|
assert controller._paused == ["org.mpris.MediaPlayer2.spotify"]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_resume_plays_only_previously_paused(monkeypatch):
|
|
from whisper_local.media._mpris import MprisController
|
|
|
|
spotify = _make_player("Paused")
|
|
vlc = _make_player("Paused")
|
|
|
|
async def fake_get_player(name: str):
|
|
return {"org.mpris.MediaPlayer2.spotify": spotify,
|
|
"org.mpris.MediaPlayer2.vlc": vlc}[name]
|
|
|
|
controller = MprisController()
|
|
controller._paused = ["org.mpris.MediaPlayer2.spotify"]
|
|
monkeypatch.setattr(controller, "_get_player_interface", fake_get_player)
|
|
|
|
await controller.resume()
|
|
|
|
spotify.call_play.assert_awaited_once()
|
|
vlc.call_play.assert_not_awaited()
|
|
assert controller._paused == []
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_resume_with_empty_paused_list_is_noop(monkeypatch):
|
|
from whisper_local.media._mpris import MprisController
|
|
|
|
controller = MprisController()
|
|
controller._paused = []
|
|
get_player = AsyncMock()
|
|
monkeypatch.setattr(controller, "_get_player_interface", get_player)
|
|
|
|
await controller.resume()
|
|
|
|
get_player.assert_not_awaited()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_pause_logs_and_continues_when_single_player_fails(monkeypatch, caplog):
|
|
from whisper_local.media._mpris import MprisController
|
|
|
|
good = _make_player("Playing")
|
|
|
|
async def fake_get_player(name: str):
|
|
if name.endswith("broken"):
|
|
raise RuntimeError("player disappeared")
|
|
return good
|
|
|
|
controller = MprisController()
|
|
monkeypatch.setattr(
|
|
controller,
|
|
"_list_player_names",
|
|
AsyncMock(return_value=[
|
|
"org.mpris.MediaPlayer2.broken",
|
|
"org.mpris.MediaPlayer2.good",
|
|
]),
|
|
)
|
|
monkeypatch.setattr(controller, "_get_player_interface", fake_get_player)
|
|
|
|
with caplog.at_level("WARNING"):
|
|
await controller.pause()
|
|
|
|
good.call_pause.assert_awaited_once()
|
|
assert controller._paused == ["org.mpris.MediaPlayer2.good"]
|
|
assert any("broken" in r.message for r in caplog.records)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_resume_logs_and_continues_when_single_player_fails(monkeypatch, caplog):
|
|
from whisper_local.media._mpris import MprisController
|
|
|
|
good = _make_player("Paused")
|
|
|
|
async def fake_get_player(name: str):
|
|
if name.endswith("broken"):
|
|
raise RuntimeError("player disappeared")
|
|
return good
|
|
|
|
controller = MprisController()
|
|
controller._paused = [
|
|
"org.mpris.MediaPlayer2.broken",
|
|
"org.mpris.MediaPlayer2.good",
|
|
]
|
|
monkeypatch.setattr(controller, "_get_player_interface", fake_get_player)
|
|
|
|
with caplog.at_level("WARNING"):
|
|
await controller.resume()
|
|
|
|
good.call_play.assert_awaited_once()
|
|
assert controller._paused == []
|
|
assert any("broken" in r.message for r in caplog.records)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_pause_is_noop_when_bus_unreachable(monkeypatch, caplog):
|
|
from whisper_local.media._mpris import MprisController
|
|
|
|
async def failing_connect(self):
|
|
raise RuntimeError("no session bus")
|
|
|
|
monkeypatch.setattr(
|
|
"dbus_next.aio.MessageBus.connect", failing_connect, raising=False
|
|
)
|
|
controller = MprisController()
|
|
|
|
with caplog.at_level("WARNING"):
|
|
await controller.pause()
|
|
|
|
assert controller._paused == []
|
|
assert any("D-Bus" in r.message or "bus" in r.message.lower()
|
|
for r in caplog.records)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_pause_skips_reconnect_after_bus_failure(monkeypatch):
|
|
"""Bus-Connect-Fehler darf nicht bei jedem Aufruf erneut versucht werden."""
|
|
from whisper_local.media._mpris import MprisController
|
|
|
|
connect_calls = 0
|
|
|
|
async def failing_connect(self):
|
|
nonlocal connect_calls
|
|
connect_calls += 1
|
|
raise RuntimeError("no session bus")
|
|
|
|
monkeypatch.setattr(
|
|
"dbus_next.aio.MessageBus.connect", failing_connect, raising=False
|
|
)
|
|
controller = MprisController()
|
|
|
|
await controller.pause()
|
|
await controller.pause()
|
|
await controller.pause()
|
|
|
|
assert connect_calls == 1
|