Some checks failed
SonarQube Code Quality Scan / SonarQube Scan (push) Failing after 1m20s
- Fix dead exception handler in discord_rpc.py (unreachable DiscordNotFound) - Replace QMediaPlayer=None stub with proper _StubQMediaPlayer in player.py - Make AudioVisualizer color stops Qt-free (raw tuples instead of QColor) - Add conftest QApplication creation check for headless CI detection - Add test_gui_widgets.py _qt_available() guard for headless environments - Add test_main.py (30 tests) for main.py entry point and env vars - Add test_import_fallbacks.py (14 tests) for Qt import fallback paths - Extend test_gui_helpers.py with _css, _get_thumbnail_nam, _cached_thumb_path tests - Extend test_discord_rpc.py with direct _connect() test for DiscordNotFound - Extend test_player_helpers.py with SearchWorker test-mode tests - Ensure import fallback tests clean up sys.modules to avoid cross-test pollution 100% coverage achieved on: config.py, discord_rpc.py, main.py, music_db.py
292 lines
11 KiB
Python
292 lines
11 KiB
Python
"""Tests for Discord RPC module (discord_rpc.py)."""
|
|
|
|
import time
|
|
from unittest import mock
|
|
|
|
import pytest
|
|
|
|
|
|
# ── _safe_artists ─────────────────────────────────────────────────────────────
|
|
|
|
class TestSafeArtists:
|
|
"""Tests for the _safe_artists helper."""
|
|
|
|
def _import(self):
|
|
from discord_rpc import _safe_artists
|
|
return _safe_artists
|
|
|
|
def test_none_song(self):
|
|
fn = self._import()
|
|
assert fn(None) == ""
|
|
|
|
def test_empty_dict(self):
|
|
fn = self._import()
|
|
assert fn({}) == ""
|
|
|
|
def test_missing_artists_key(self):
|
|
fn = self._import()
|
|
assert fn({"title": "Test"}) == ""
|
|
|
|
def test_artists_not_list(self):
|
|
fn = self._import()
|
|
assert fn({"artists": "not a list"}) == ""
|
|
|
|
def test_empty_artists_list(self):
|
|
fn = self._import()
|
|
assert fn({"artists": []}) == ""
|
|
|
|
def test_single_artist(self):
|
|
fn = self._import()
|
|
song = {"artists": [{"name": "Test Artist", "id": "x"}]}
|
|
assert fn(song) == "Test Artist"
|
|
|
|
def test_multiple_artists(self):
|
|
fn = self._import()
|
|
song = {
|
|
"artists": [
|
|
{"name": "Artist One", "id": "1"},
|
|
{"name": "Artist Two", "id": "2"},
|
|
]
|
|
}
|
|
assert fn(song) == "Artist One, Artist Two"
|
|
|
|
def test_artist_without_name(self):
|
|
fn = self._import()
|
|
song = {"artists": [{"id": "x"}, {"name": "Real Artist", "id": "y"}]}
|
|
assert fn(song) == "Real Artist"
|
|
|
|
def test_artist_is_not_dict(self):
|
|
fn = self._import()
|
|
song = {"artists": ["just a string"]}
|
|
assert fn(song) == ""
|
|
|
|
def test_artist_with_empty_name(self):
|
|
fn = self._import()
|
|
song = {"artists": [{"name": "", "id": "x"}]}
|
|
assert fn(song) == ""
|
|
|
|
|
|
# ── DiscordRPC ────────────────────────────────────────────────────────────────
|
|
|
|
class TestDiscordRPC:
|
|
"""Tests for DiscordRPC class with mocked pypresence."""
|
|
|
|
@pytest.fixture
|
|
def rpc(self):
|
|
with mock.patch("discord_rpc.Presence") as mock_presence:
|
|
# Make the mock Presence instance return a mock
|
|
mock_instance = mock.MagicMock()
|
|
mock_presence.return_value = mock_instance
|
|
|
|
from discord_rpc import DiscordRPC
|
|
rpc = DiscordRPC(client_id="test_client_123")
|
|
yield rpc, mock_instance
|
|
rpc.stop()
|
|
|
|
def test_constructor(self, rpc):
|
|
drpc, _ = rpc
|
|
assert drpc._client_id == "test_client_123"
|
|
assert drpc._connected is False
|
|
assert drpc._song is None
|
|
|
|
def test_start_and_connect(self, rpc):
|
|
drpc, mock_presence = rpc
|
|
drpc.start()
|
|
# Give the thread a moment to connect
|
|
import time
|
|
time.sleep(0.1)
|
|
assert drpc._connected is True
|
|
mock_presence.connect.assert_called_once()
|
|
|
|
def test_update_song_sets_song_and_resets_start(self, rpc):
|
|
drpc, _ = rpc
|
|
song = {"title": "Test", "artists": [{"name": "A"}], "duration": 200}
|
|
drpc.update_song(song, reset_start=True)
|
|
assert drpc._song == song
|
|
assert drpc._start_ts is not None
|
|
|
|
def test_update_song_none(self, rpc):
|
|
drpc, _ = rpc
|
|
drpc.update_song(None)
|
|
assert drpc._song is None
|
|
assert drpc._start_ts is None
|
|
|
|
def test_update_song_no_reset(self, rpc):
|
|
drpc, _ = rpc
|
|
song = {"title": "Test", "artists": [{"name": "A"}], "duration": 200}
|
|
drpc.update_song(song, reset_start=True)
|
|
old_start = drpc._start_ts
|
|
time.sleep(0.01)
|
|
# Without reset, start_ts should stay the same
|
|
drpc.update_song(song, reset_start=False)
|
|
assert drpc._start_ts == old_start
|
|
|
|
def test_clear_presence(self, rpc):
|
|
drpc, _ = rpc
|
|
drpc.update_song({"title": "Test"})
|
|
assert drpc._song is not None
|
|
drpc.clear()
|
|
assert drpc._song is None
|
|
assert drpc._start_ts is None
|
|
|
|
def test_seek_to_adjusts_start_ts(self, rpc):
|
|
drpc, _ = rpc
|
|
drpc.update_song({"title": "Test"}, reset_start=True)
|
|
time.sleep(0.01)
|
|
drpc.seek_to(position_ms=30_000) # seek to 30s
|
|
# Now the diff between now and start_ts should be ~30s
|
|
diff = int(time.time()) - drpc._start_ts
|
|
assert 28 <= diff <= 32 # allow some fuzz
|
|
|
|
def test_stop_disconnects_and_joins(self, rpc):
|
|
drpc, _ = rpc
|
|
drpc.start()
|
|
time.sleep(0.1)
|
|
drpc.stop()
|
|
assert drpc._rpc is None
|
|
assert drpc._connected is False
|
|
|
|
def test_send_presence_clears_when_no_song(self, rpc):
|
|
drpc, _mock = rpc
|
|
drpc.start()
|
|
time.sleep(0.1)
|
|
drpc.clear()
|
|
time.sleep(0.1)
|
|
_mock.clear.assert_called()
|
|
|
|
def test_double_start_is_idempotent(self, rpc):
|
|
drpc, _ = rpc
|
|
drpc.start()
|
|
drpc.start() # should not crash
|
|
# If we get here without exception the test passes
|
|
|
|
@mock.patch("discord_rpc.Presence")
|
|
def test_connect_discord_not_found(self, mock_presence_cls):
|
|
"""When Discord is not running, DiscordNotFound is caught (covers lines 116-118)."""
|
|
from discord_rpc import DiscordRPC, DiscordNotFound
|
|
mock_instance = mock.MagicMock()
|
|
mock_instance.connect.side_effect = DiscordNotFound()
|
|
mock_presence_cls.return_value = mock_instance
|
|
|
|
drpc = DiscordRPC(client_id="test")
|
|
drpc.start()
|
|
time.sleep(0.1)
|
|
# Should not crash, just log a warning
|
|
assert drpc._connected is False
|
|
drpc.stop()
|
|
|
|
def test_disconnect_close_exception_handled(self):
|
|
"""_disconnect handles close() failure gracefully (covers lines 128-129)."""
|
|
with mock.patch("discord_rpc.Presence") as mock_presence_cls:
|
|
mock_instance = mock.MagicMock()
|
|
mock_instance.close.side_effect = RuntimeError("close failed")
|
|
mock_presence_cls.return_value = mock_instance
|
|
|
|
from discord_rpc import DiscordRPC
|
|
drpc = DiscordRPC(client_id="test")
|
|
drpc._rpc = mock_instance
|
|
drpc._connected = True
|
|
drpc._disconnect()
|
|
assert drpc._connected is False
|
|
assert drpc._rpc is None
|
|
|
|
def test_send_presence_clear_exception_handled(self):
|
|
"""_send_presence handles clear() failure gracefully (covers lines 142-143)."""
|
|
with mock.patch("discord_rpc.Presence") as mock_presence_cls:
|
|
mock_instance = mock.MagicMock()
|
|
mock_instance.clear.side_effect = RuntimeError("clear failed")
|
|
mock_presence_cls.return_value = mock_instance
|
|
|
|
from discord_rpc import DiscordRPC
|
|
drpc = DiscordRPC(client_id="test")
|
|
drpc._rpc = mock_instance
|
|
drpc._connected = True
|
|
drpc._song = None # no song -> goes to clear branch
|
|
drpc._send_presence()
|
|
assert drpc._connected is False
|
|
|
|
def test_send_presence_update_exception_handled(self):
|
|
"""_send_presence handles update() failure gracefully (covers lines 160-167)."""
|
|
with mock.patch("discord_rpc.Presence") as mock_presence_cls:
|
|
mock_instance = mock.MagicMock()
|
|
mock_instance.update.side_effect = RuntimeError("update failed")
|
|
mock_presence_cls.return_value = mock_instance
|
|
|
|
from discord_rpc import DiscordRPC
|
|
drpc = DiscordRPC(client_id="test")
|
|
drpc._rpc = mock_instance
|
|
drpc._connected = True
|
|
drpc._song = {"title": "Test", "artists": [{"name": "A"}], "duration": 100}
|
|
drpc._start_ts = 1000
|
|
drpc._send_presence()
|
|
assert drpc._connected is False
|
|
|
|
def test_send_presence_rpc_is_none(self):
|
|
"""_send_presence does nothing when _rpc is None (covers line ~155)."""
|
|
from discord_rpc import DiscordRPC
|
|
drpc = DiscordRPC(client_id="test")
|
|
drpc._rpc = None
|
|
drpc._song = {"title": "Test"}
|
|
drpc._start_ts = 1000
|
|
# Should not crash
|
|
drpc._send_presence()
|
|
|
|
def test_connect_oserror_handled(self):
|
|
"""_connect handles OSError gracefully (covers lines 119-122)."""
|
|
with mock.patch("discord_rpc.Presence") as mock_presence_cls:
|
|
mock_instance = mock.MagicMock()
|
|
mock_instance.connect.side_effect = OSError("Connection refused")
|
|
mock_presence_cls.return_value = mock_instance
|
|
|
|
from discord_rpc import DiscordRPC
|
|
drpc = DiscordRPC(client_id="test")
|
|
result = drpc._connect()
|
|
assert result is False
|
|
assert drpc._connected is False
|
|
|
|
def test_connect_runtimeerror_handled(self):
|
|
"""_connect handles RuntimeError gracefully (covers lines 119-122)."""
|
|
with mock.patch("discord_rpc.Presence") as mock_presence_cls:
|
|
mock_instance = mock.MagicMock()
|
|
mock_instance.connect.side_effect = RuntimeError("pipe error")
|
|
mock_presence_cls.return_value = mock_instance
|
|
|
|
from discord_rpc import DiscordRPC
|
|
drpc = DiscordRPC(client_id="test")
|
|
result = drpc._connect()
|
|
assert result is False
|
|
assert drpc._connected is False
|
|
|
|
def test_connect_discord_not_found_direct(self):
|
|
"""_connect handles DiscordNotFound when called directly (not through thread)."""
|
|
with mock.patch("discord_rpc.Presence") as mock_presence_cls:
|
|
from discord_rpc import DiscordRPC, DiscordNotFound
|
|
mock_instance = mock.MagicMock()
|
|
mock_instance.connect.side_effect = DiscordNotFound()
|
|
mock_presence_cls.return_value = mock_instance
|
|
|
|
drpc = DiscordRPC(client_id="test")
|
|
result = drpc._connect()
|
|
assert result is False
|
|
assert drpc._connected is False
|
|
|
|
def test_run_reconnects_on_failure(self):
|
|
"""_run loop retries connection when _connect fails (covers lines 174-175)."""
|
|
with mock.patch("discord_rpc.Presence") as mock_presence_cls:
|
|
mock_instance = mock.MagicMock()
|
|
mock_instance.connect.side_effect = OSError("no discord")
|
|
mock_presence_cls.return_value = mock_instance
|
|
|
|
from discord_rpc import DiscordRPC
|
|
drpc = DiscordRPC(client_id="test")
|
|
# Start _run in a background thread so we can signal stop
|
|
# from the main thread while _run blocks on wait().
|
|
import threading
|
|
t = threading.Thread(target=drpc._run, daemon=True)
|
|
t.start()
|
|
time.sleep(0.1) # let _connect fail and enter wait(10)
|
|
drpc._stop_event.set() # unblock wait() so _run can exit
|
|
t.join(timeout=2)
|
|
assert drpc._connected is False
|
|
assert drpc._rpc is None
|