from unittest.mock import MagicMock, patch from lintunes.models import Track from lintunes.models.library import Library from lintunes import player as player_module from lintunes.player import Player class FakeManager: """Minimal stand-in for LibraryManager: the Player reads `library.tracks`, calls `record_play` on natural end-of-media, and `set_track_location` when the user relocates a moved/renamed file.""" def __init__(self, library): self.library = library self.played = [] def record_play(self, track_id): self.played.append(track_id) def set_track_location(self, track_id, location): self.library.tracks[track_id].location = location def _make_player(qapp, tracks): library = Library() for track in tracks: library.tracks[track.track_id] = track # The Qt multimedia backend (QMediaPlayer/QAudioOutput/QMediaDevices) # blocks on init in a headless environment, so stub it out — the # missing-file path under test never reaches real playback anyway. with patch.multiple( player_module, QMediaPlayer=MagicMock(), QAudioOutput=MagicMock(), QAudioBufferOutput=MagicMock(), QMediaDevices=MagicMock(), ): player = Player(FakeManager(library)) errors, missing = [], [] player.error_occurred.connect(errors.append) player.track_missing.connect(missing.append) return player, errors, missing def test_missing_file_emits_track_missing_and_stops(qapp, tmp_path): location = str(tmp_path / "not-mounted" / "song.mp3") track = Track(track_id=1, name="Ghost Song", location=location) player, errors, missing = _make_player(qapp, [track]) player.play_queue([1], 0) # Routed to track_missing (so the UI can offer "Locate File…"), not the # generic error dialog. assert errors == [] assert missing == [track] # Stopped, not advanced into a track; setSource was never called. assert player.current_track is None player._media.setSource.assert_not_called() def test_all_missing_queue_does_not_recurse(qapp, tmp_path): # A whole-library queue with every file gone (drive unmounted) must not # cascade through the queue and blow the recursion limit. tracks = [ Track(track_id=i, name=f"T{i}", location=str(tmp_path / f"{i}.mp3")) for i in range(5000) ] player, errors, missing = _make_player(qapp, tracks) player.play_queue([t.track_id for t in tracks], 0) # One alert for the track we tried to play, then a clean stop. assert len(missing) == 1 assert player.current_track is None def test_track_not_in_library_emits_error(qapp): # Queue references an id that isn't in the library: nothing to locate, so # this stays a plain error (not track_missing). player, errors, missing = _make_player(qapp, []) player.play_queue([99], 0) assert missing == [] assert len(errors) == 1 assert player.current_track is None def test_retry_current_plays_after_relocate(qapp, tmp_path): real = tmp_path / "real.mp3" real.write_bytes(b"audio") track = Track(track_id=1, name="Moved Song", location=str(tmp_path / "gone.mp3")) player, errors, missing = _make_player(qapp, [track]) player.play_queue([1], 0) assert missing == [track] and player.current_track is None # User relocates the file, then the UI retries the same queue position. player._manager.set_track_location(1, str(real)) missing.clear() player.retry_current() assert errors == [] and missing == [] assert player.current_track is track player._media.setSource.assert_called_once()