/
/
/
1"""
2Tests for surfacing receiver media errors from ChromecastPlayer.
3
4Regression tests for https://github.com/music-assistant/support/issues/5981, where a
5receiver answered every LOAD with LOAD_FAILED and a media status carrying
6``idleReason: ERROR``, and Music Assistant logged nothing: the player silently
7returned to idle. A media error reported by the receiver must be visible in the log.
8
9The LOAD_FAILED message itself only reaches the ``load_media_failed`` listener when
10the receiver includes a detailed error code, so the media status path is the one
11that must catch the general case.
12"""
13
14from __future__ import annotations
15
16from typing import cast
17from unittest.mock import MagicMock
18
19from music_assistant.providers.chromecast.player import ChromecastPlayer
20from tests.providers.chromecast.helpers import bind_media_status_helpers
21
22
23def _handle_media_status(fake: MagicMock, status: MagicMock) -> None:
24 ChromecastPlayer._handle_media_status(cast("ChromecastPlayer", fake), status)
25
26
27def _fake_player() -> MagicMock:
28 """Build a MagicMock standing in for an idle, ungrouped ChromecastPlayer."""
29 fake = MagicMock()
30 fake.active_cast_group = None
31 fake._media_error_reported = False
32 fake._flow_stream_underrun = MagicMock(return_value=False)
33 return bind_media_status_helpers(fake)
34
35
36def _error_status() -> MagicMock:
37 status = MagicMock()
38 status.content_id = "http://192.168.1.58:8097/flow/abc/track.flac"
39 status.player_is_playing = False
40 status.player_is_paused = False
41 status.player_is_idle = True
42 status.idle_reason = "ERROR"
43 return status
44
45
46def _playing_status() -> MagicMock:
47 status = MagicMock()
48 status.content_id = "http://192.168.1.58:8097/flow/abc/track.flac"
49 status.player_is_playing = True
50 status.player_is_paused = False
51 status.player_is_idle = False
52 status.idle_reason = None
53 return status
54
55
56def test_idle_error_is_logged() -> None:
57 """A media status with idleReason ERROR produces a warning naming the media."""
58 fake = _fake_player()
59
60 _handle_media_status(fake, _error_status())
61
62 fake.logger.warning.assert_called_once()
63 assert "track.flac" in str(fake.logger.warning.call_args)
64
65
66def test_repeated_error_status_is_logged_once() -> None:
67 """The receiver echoes the error status several times; only the first is logged."""
68 fake = _fake_player()
69
70 _handle_media_status(fake, _error_status())
71 _handle_media_status(fake, _error_status())
72 _handle_media_status(fake, _error_status())
73
74 fake.logger.warning.assert_called_once()
75
76
77def test_error_logged_again_after_recovery() -> None:
78 """A new error after successful playback is a new incident and is logged again."""
79 fake = _fake_player()
80
81 _handle_media_status(fake, _error_status())
82 _handle_media_status(fake, _playing_status())
83 _handle_media_status(fake, _error_status())
84
85 assert fake.logger.warning.call_count == 2
86
87
88def test_group_error_is_not_logged_by_every_member() -> None:
89 """A group's error reaches all its members, but only the group itself reports it."""
90 group = MagicMock()
91 # a plain spec= mock has no 'cc', which is set in __init__
92 group.__class__ = ChromecastPlayer # type: ignore[assignment]
93 group.cc.media_controller.status = _error_status()
94 member = _fake_player()
95 member.active_cast_group = "group-uuid"
96 member.mass.players.get_player = MagicMock(return_value=group)
97
98 _handle_media_status(member, _error_status())
99
100 member.logger.warning.assert_not_called()
101 # assert the group status was really processed, so the check above cannot pass
102 # just because the member bailed out before reaching the error handling
103 member.mass.players.get_player.assert_called_once_with("group-uuid")
104 member.update_state.assert_called_once()
105
106
107def test_flow_stream_underrun_is_not_an_error() -> None:
108 """An idle ERROR at the end of a fully consumed flow stream is expected, not logged."""
109 fake = _fake_player()
110 fake._flow_stream_underrun = MagicMock(return_value=True)
111
112 _handle_media_status(fake, _error_status())
113
114 fake.logger.warning.assert_not_called()
115
116
117def test_load_media_failed_logs_the_error_code() -> None:
118 """A LOAD_FAILED with a detailed error code is logged with its meaning."""
119 fake = _fake_player()
120
121 ChromecastPlayer._handle_load_media_failed(cast("ChromecastPlayer", fake), 1, 104)
122
123 fake.logger.warning.assert_called_once()
124 assert "104" in str(fake.logger.warning.call_args)
125 assert fake._media_error_reported is True
126