/
/
/
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
20
21
22def _handle_media_status(fake: MagicMock, status: MagicMock) -> None:
23 ChromecastPlayer._handle_media_status(cast("ChromecastPlayer", fake), status)
24
25
26def _fake_player() -> MagicMock:
27 """Build a MagicMock standing in for an idle, ungrouped ChromecastPlayer."""
28 fake = MagicMock()
29 fake.active_cast_group = None
30 fake._media_error_reported = False
31 fake._flow_stream_underrun = MagicMock(return_value=False)
32 return fake
33
34
35def _error_status() -> MagicMock:
36 status = MagicMock()
37 status.content_id = "http://192.168.1.58:8097/flow/abc/track.flac"
38 status.player_is_playing = False
39 status.player_is_paused = False
40 status.player_is_idle = True
41 status.idle_reason = "ERROR"
42 return status
43
44
45def _playing_status() -> MagicMock:
46 status = MagicMock()
47 status.content_id = "http://192.168.1.58:8097/flow/abc/track.flac"
48 status.player_is_playing = True
49 status.player_is_paused = False
50 status.player_is_idle = False
51 status.idle_reason = None
52 return status
53
54
55def test_idle_error_is_logged() -> None:
56 """A media status with idleReason ERROR produces a warning naming the media."""
57 fake = _fake_player()
58
59 _handle_media_status(fake, _error_status())
60
61 fake.logger.warning.assert_called_once()
62 assert "track.flac" in str(fake.logger.warning.call_args)
63
64
65def test_repeated_error_status_is_logged_once() -> None:
66 """The receiver echoes the error status several times; only the first is logged."""
67 fake = _fake_player()
68
69 _handle_media_status(fake, _error_status())
70 _handle_media_status(fake, _error_status())
71 _handle_media_status(fake, _error_status())
72
73 fake.logger.warning.assert_called_once()
74
75
76def test_error_logged_again_after_recovery() -> None:
77 """A new error after successful playback is a new incident and is logged again."""
78 fake = _fake_player()
79
80 _handle_media_status(fake, _error_status())
81 _handle_media_status(fake, _playing_status())
82 _handle_media_status(fake, _error_status())
83
84 assert fake.logger.warning.call_count == 2
85
86
87def test_group_error_is_not_logged_by_every_member() -> None:
88 """A group's error reaches all its members, but only the group itself reports it."""
89 group = MagicMock()
90 # a plain spec= mock has no 'cc', which is set in __init__
91 group.__class__ = ChromecastPlayer # type: ignore[assignment]
92 group.cc.media_controller.status = _error_status()
93 member = _fake_player()
94 member.active_cast_group = "group-uuid"
95 member.mass.players.get_player = MagicMock(return_value=group)
96
97 _handle_media_status(member, _error_status())
98
99 member.logger.warning.assert_not_called()
100 # assert the group status was really processed, so the check above cannot pass
101 # just because the member bailed out before reaching the error handling
102 member.mass.players.get_player.assert_called_once_with("group-uuid")
103 member.update_state.assert_called_once()
104
105
106def test_flow_stream_underrun_is_not_an_error() -> None:
107 """An idle ERROR at the end of a fully consumed flow stream is expected, not logged."""
108 fake = _fake_player()
109 fake._flow_stream_underrun = MagicMock(return_value=True)
110
111 _handle_media_status(fake, _error_status())
112
113 fake.logger.warning.assert_not_called()
114
115
116def test_load_media_failed_logs_the_error_code() -> None:
117 """A LOAD_FAILED with a detailed error code is logged with its meaning."""
118 fake = _fake_player()
119
120 ChromecastPlayer._handle_load_media_failed(cast("ChromecastPlayer", fake), 1, 104)
121
122 fake.logger.warning.assert_called_once()
123 assert "104" in str(fake.logger.warning.call_args)
124 assert fake._media_error_reported is True
125