| Boost coverage to 60% with 256 passing tests
All checks were successful
SonarQube Code Quality Scan / SonarQube Scan (push) Successful in 1m52s

- mpris.py: 81% → 100% — import fallbacks, flush_props edge cases,
  stop_async error path, run_async exception path, notify loops
- player.py: 51% → 73% — AudioPlayer state/volume/loop/queue tests,
  _on_download_failed, _on_download_succeeded, _on_playback_state,
  _on_media_status, seek/skip/previous, _start_prefetch, _advance,
  _cancel_active_downloads, DownloadWorker._do_cancel
- test_gui_widgets.py: 50 → 88 tests across TestAudioPlayer and
  existing widget tests
- 5 modules at 100%: config, discord_rpc, main, mpris, music_db
This commit is contained in:
2026-06-09 19:33:58 +03:00
parent 27d12d31cb
commit 4671a6937b
3 changed files with 574 additions and 0 deletions

View File

@@ -212,6 +212,338 @@ class TestSearchPage:
# ═══════════════════════════════════════════════════════════════════════════════
class TestAudioPlayer:
"""Tests for AudioPlayer state methods (no playback required)."""
@pytest.fixture
def player(self, app):
from player import AudioPlayer
p = AudioPlayer()
yield p
p.shutdown()
def test_initial_state(self, player):
"""Initial player state is stopped, no current song, empty queue."""
assert player.is_stopped() is True
assert player.is_playing() is False
assert player.is_paused() is False
assert player.get_current() is None
assert player.get_queue() == []
assert player.get_queue_length() == 0
assert player.has_previous() is False
def test_volume_default(self, player):
"""Default volume should be 50."""
assert player.volume() == 50
def test_set_volume(self, player):
"""set_volume changes the volume."""
player.set_volume(75)
assert player.volume() == 75
player.set_volume(100)
assert player.volume() == 100
player.set_volume(0)
assert player.volume() == 0
def test_set_volume_clamps(self, player):
"""set_volume clamps values outside 0-100."""
player.set_volume(-10)
assert player.volume() == 0
player.set_volume(150)
assert player.volume() == 100
def test_loop_default(self, player):
"""Loop mode is initially off."""
assert player.get_loop() is False
def test_set_loop(self, player):
"""set_loop toggles loop mode."""
player.set_loop(True)
assert player.get_loop() is True
player.set_loop(False)
assert player.get_loop() is False
def test_stop_playback_when_stopped(self, player):
"""stop_playback when already stopped doesn't crash."""
player.stop_playback()
assert player.is_stopped() is True
def test_queue_operations(self, player):
"""queue_song, queue_next, and queue_list work correctly.
``queue_song`` triggers auto-play when queue is empty, which pops
the song immediately. We test queue_list (no auto-play) instead.
"""
s2 = {"videoId": "b", "title": "B"}
s3 = {"videoId": "c", "title": "C"}
# queue_list adds without triggering auto-play
player.queue_list([s2, s3])
assert player.get_queue_length() == 2
assert player.get_queue()[0]["videoId"] == "b"
# queue_next inserts at position 0
s1 = {"videoId": "a", "title": "A"}
player.queue_next(s1)
assert player.get_queue_length() == 3
assert player.get_queue()[0]["videoId"] == "a"
def test_seek(self, player):
"""seek sets position (no crash test)."""
player.seek(0) # should not crash
player.seek(5000)
def test_shutdown(self, player):
"""shutdown cleans up without error."""
player.shutdown() # already called in fixture cleanup
def test_cleanup_current_temp_no_current(self, player):
"""_cleanup_current_temp does nothing when no song is loaded."""
player._cleanup_current_temp() # should not crash
def test_cleanup_temp_files_no_files(self, player):
"""_cleanup_temp_files handles missing files gracefully."""
player._current = {"_local_path": "/nonexistent/file.mp3"}
player._cleanup_temp_files() # should not crash
player._current = None
def test_cleanup_temp_files_with_cache(self, player, tmp_path):
"""_cleanup_temp_files removes cached temp files."""
f = tmp_path / "test_song.mp3"
f.write_text("audio data")
player._prefetch_cache["vid1"] = {"_local_path": str(f)}
player._cleanup_temp_files()
assert not f.exists()
def test_position_and_duration_signals(self, player):
"""_on_position and _on_duration emit signals."""
results = []
player.position_changed.connect(lambda ms: results.append(("pos", ms)))
player.duration_changed.connect(lambda ms: results.append(("dur", ms)))
player._on_position(5000)
player._on_duration(200000)
assert ("pos", 5000) in results
assert ("dur", 200000) in results
def test_is_loaded(self, player):
"""is_loaded returns False when no current song."""
assert player.is_loaded() is False
player._current = {"videoId": "x"}
assert player.is_loaded() is True
player._current = None
def test_toggle_pause_when_stopped(self, player):
"""toggle_pause when stopped does nothing."""
result = player.toggle_pause()
# When stopped, toggle_pause returns None (no state change)
assert result is None or result is False
def test_previous_no_history(self, player):
"""previous does nothing when history is empty."""
player.previous() # should not crash
assert player.has_previous() is False
def test_cancel_active_downloads(self, player):
"""_cancel_active_downloads emits cancel signal when active."""
player._active_task_id = 42
with mock.patch.object(player._worker, "cancel_requested") as mock_sig:
player._cancel_active_downloads()
mock_sig.emit.assert_called_once()
def test_cancel_active_downloads_noop(self, player):
"""_cancel_active_downloads does nothing when no active task."""
with mock.patch.object(player._worker, "cancel_requested") as mock_sig:
player._cancel_active_downloads()
mock_sig.emit.assert_not_called()
def test_on_download_failed_matching_task(self, player):
"""_on_download_failed emits error when task matches active."""
results = []
player.error.connect(lambda d: results.append(d))
player._active_task_id = 42
player._on_download_failed(42)
assert len(results) == 1
assert "error" in results[0]
assert player._active_task_id is None
def test_on_download_failed_stale_task(self, player):
"""_on_download_failed ignores stale task IDs."""
results = []
player.error.connect(lambda d: results.append(d))
player._active_task_id = 42
player._on_download_failed(99) # different task ID
assert results == [] # no signal emitted
assert player._active_task_id == 42 # unchanged
def test_skip_no_current(self, player):
"""skip does nothing when no current song."""
player.skip() # should not crash
def test_on_download_succeeded_stale(self, player):
"""_on_download_succeeded ignores stale task IDs."""
player._active_task_id = 42
player._on_download_succeeded(99, {"videoId": "x"}) # different task
# No crash, no state change
def test_on_download_succeeded_missing_file(self, player, tmp_path):
"""_on_download_succeeded emits error when file is missing."""
results = []
player.error.connect(lambda d: results.append(d))
player._active_task_id = 42
player._on_download_succeeded(42, {"videoId": "x",
"_local_path": str(tmp_path / "nonexistent.mp3")})
assert len(results) == 1
assert player._active_task_id is None
def test_on_playback_state_playing(self, player):
"""_on_playback_state with PlayingState emits 'playing'."""
from PySide6.QtMultimedia import QMediaPlayer
results = []
player.playback_state_changed.connect(lambda s: results.append(s))
player._current = {"videoId": "x", "title": "Test"}
player._on_playback_state(QMediaPlayer.PlaybackState.PlayingState)
assert "playing" in results
player._current = None
def test_on_playback_state_paused(self, player):
"""_on_playback_state with PausedState emits 'paused'."""
from PySide6.QtMultimedia import QMediaPlayer
results = []
player.playback_state_changed.connect(lambda s: results.append(s))
player._current = {"videoId": "x"}
player._on_playback_state(QMediaPlayer.PlaybackState.PausedState)
assert "paused" in results
player._current = None
def test_on_playback_state_stopped(self, player):
"""_on_playback_state with StoppedState emits 'stopped'."""
from PySide6.QtMultimedia import QMediaPlayer
results = []
player.playback_state_changed.connect(lambda s: results.append(s))
player._on_playback_state(QMediaPlayer.PlaybackState.StoppedState)
assert "stopped" in results
def test_on_media_status_end_of_media(self, player):
"""_on_media_status with EndOfMedia triggers advance."""
from PySide6.QtMultimedia import QMediaPlayer
results = []
player.song_ended.connect(lambda vid: results.append(vid))
player._current = {"videoId": "x"}
player._on_media_status(QMediaPlayer.MediaStatus.EndOfMedia)
assert "x" in results
player._current = None
def test_on_media_status_invalid_media_stopped(self, player):
"""_on_media_status with InvalidMedia does nothing when player is stopped."""
from PySide6.QtMultimedia import QMediaPlayer
results = []
player.error.connect(lambda d: results.append(d))
player._current = {"videoId": "x"}
# When the player is stopped (no media loaded), InvalidMedia is ignored
player._on_media_status(QMediaPlayer.MediaStatus.InvalidMedia)
assert len(results) == 0 # player is stopped, so early return
player._current = None
def test_on_media_status_loaded(self, player):
"""_on_media_status with LoadedMedia does nothing special."""
from PySide6.QtMultimedia import QMediaPlayer
player._on_media_status(QMediaPlayer.MediaStatus.LoadedMedia) # no crash
def test_get_position(self, player):
"""get_position returns current position (0 when stopped)."""
assert player.get_position() == 0
def test_start_prefetch(self, player):
"""_start_prefetch with queue emits download request."""
from player import DownloadWorker
worker = DownloadWorker()
player._worker = worker
player._queue = [{"videoId": "prefetch_test"}]
with mock.patch.object(worker, "download_requested") as mock_sig:
player._start_prefetch()
mock_sig.emit.assert_called_once()
task_id, song = mock_sig.emit.call_args[0]
assert isinstance(task_id, int)
assert song["videoId"] == "prefetch_test"
def test_on_download_succeeded_prefetch(self, player):
"""_on_download_succeeded caches prefetch result."""
player._prefetch_task_id = 55
player._queue = [{"videoId": "prefetch_vid"}]
player._on_download_succeeded(55, {"videoId": "prefetch_vid",
"_local_path": "/tmp/test.mp3"})
assert "prefetch_vid" in player._prefetch_cache
assert player._prefetch_task_id is None
def test_advance_with_loop(self, player):
"""_advance re-queues finished song at front in loop mode."""
player._loop_mode = True
player._current = {"videoId": "loop_song"}
player._queue = [{"videoId": "next_song"}]
# _advance inserts at front, then _try_next_song pops the front
# leaving the next_song. Verify loop_song was inserted before pop.
with mock.patch.object(player, "_try_next_song"):
player._advance()
# The finished song should be at the front before _try_next_song
assert player._queue[0]["videoId"] == "loop_song"
assert player._current is None
def test_try_next_song_with_queue(self, player):
"""_try_next_song pops and plays when queue has items."""
player._queue = [{"videoId": "queued"}]
# This will call _pop_and_play which tries to download
# Just check it doesn't crash and processes the queue
with mock.patch.object(player, "_pop_and_play") as mock_pop:
player._try_next_song()
mock_pop.assert_called_once()
def test_on_player_error_network(self, player):
"""_on_player_error_occurred with NetworkError triggers advance."""
from PySide6.QtMultimedia import QMediaPlayer
results = []
player.error.connect(lambda d: results.append(d))
player._current = {"videoId": "err_song"}
player._on_player_error_occurred(
QMediaPlayer.Error.NetworkError, "Connection lost")
assert len(results) == 1
assert "error" in results[0]
player._current = None
def test_on_player_error_format(self, player):
"""_on_player_error_occurred with FormatError does nothing."""
from PySide6.QtMultimedia import QMediaPlayer
results = []
player.error.connect(lambda d: results.append(d))
player._on_player_error_occurred(
QMediaPlayer.Error.FormatError, "Bad format")
# FormatError is not NetworkError or ResourceError, so ignored
assert len(results) == 0
def test_seek_emits_seeked_signal(self, player):
"""seek emits seeked signal with the position."""
results = []
player.seeked.connect(lambda ms: results.append(ms))
player.seek(15000)
assert 15000 in results
def test_download_worker_do_cancel(self, player):
"""DownloadWorker._do_cancel sets cancelled flag."""
from player import DownloadWorker
worker = DownloadWorker()
assert worker._cancelled is False
worker._do_cancel()
assert worker._cancelled is True
def test_on_position_zero(self, player):
"""_on_position with 0 emits position_changed."""
results = []
player.position_changed.connect(lambda ms: results.append(ms))
player._on_position(0)
assert 0 in results
class TestTunettiWindowAccessors:
@pytest.fixture