"""Round 23: playback-control provenance log. Every pathway that can start/stop playback now drops a one-line record via lintunes.eventlog (MPRIS methods with D-Bus sender, the media-key eventFilter with source device, Player transport methods with position), so "the app played by itself at night" incidents are attributable after the fact from ~/.cache/lintunes/control-events.log. """ from unittest.mock import MagicMock, patch from lintunes import eventlog from lintunes.models import Library, Track from lintunes import player as player_module from lintunes.player import Player def _patch_log(monkeypatch, tmp_path): path = tmp_path / "control-events.log" monkeypatch.setattr(eventlog, "_log_path", lambda: path) return path class TestEventLog: def test_writes_timestamped_line(self, monkeypatch, tmp_path): path = _patch_log(monkeypatch, tmp_path) eventlog.log_control("test", "action", "k=v") line = path.read_text().strip() assert line.endswith("[test] action k=v") assert line[:4].isdigit() # starts with a date def test_detail_optional(self, monkeypatch, tmp_path): path = _patch_log(monkeypatch, tmp_path) eventlog.log_control("test", "solo") assert path.read_text().strip().endswith("[test] solo") def test_rotates_when_large(self, monkeypatch, tmp_path): path = _patch_log(monkeypatch, tmp_path) path.write_bytes(b"x" * (eventlog._MAX_BYTES + 1)) eventlog.log_control("test", "rotated") assert path.with_suffix(".log.old").exists() assert "rotated" in path.read_text() def test_never_raises_on_io_error(self, monkeypatch): monkeypatch.setattr(eventlog, "_log_path", lambda: type("P", (), {})()) # broken path object eventlog.log_control("test", "no-crash") # must not raise class TestMprisLogging: def test_playpause_logs_and_forwards(self, qapp, monkeypatch, tmp_path): path = _patch_log(monkeypatch, tmp_path) from lintunes.mpris import MprisService player = MagicMock() service = MprisService(player, MagicMock()) service.player_adaptor.PlayPause() player.toggle_play.assert_called_once() text = path.read_text() assert "[mpris] PlayPause" in text def test_all_methods_log(self, qapp, monkeypatch, tmp_path): path = _patch_log(monkeypatch, tmp_path) from lintunes.mpris import MprisService service = MprisService(MagicMock(), MagicMock()) adaptor = service.player_adaptor adaptor.Play() adaptor.Pause() adaptor.Next() adaptor.Previous() adaptor.Stop() text = path.read_text() for member in ("Play", "Pause", "Next", "Previous", "Stop"): assert f"[mpris] {member}" in text class TestPlayerLogging: def _player(self, qapp, tracks): library = Library() for t in tracks: library.tracks[t.track_id] = t manager = MagicMock() manager.library = library with patch.multiple( player_module, QMediaPlayer=MagicMock(), QAudioOutput=MagicMock(), QAudioBufferOutput=MagicMock(), QMediaDevices=MagicMock(), ): return Player(manager) def test_toggle_play_logs_state_and_position(self, qapp, monkeypatch, tmp_path): path = _patch_log(monkeypatch, tmp_path) (tmp_path / "a.mp3").write_bytes(b"x") track = Track(track_id=1, name="Song A", location=str(tmp_path / "a.mp3"), total_time=100_000) player = self._player(qapp, [track]) player._media.position.return_value = 4321 player._media.playbackState.return_value = None # != PlayingState player.play_queue([1], 0) player.toggle_play() text = path.read_text() assert "[player] toggle_play" in text assert "pos=4321ms" in text and "'Song A'" in text