188 lines
7.3 KiB
Python
188 lines
7.3 KiB
Python
"""Parser tests driven by real Shield firmware captures (Android 11, RQ1A.210105.003).
|
|
|
|
Fixtures live in tests/fixtures — see docs/RESEARCH_SHIELD.md for how each
|
|
capture was produced and which expectations it pins.
|
|
"""
|
|
|
|
from pathlib import Path
|
|
|
|
from ha_adb_common.media_session import (
|
|
MediaSessionInfo,
|
|
parse_all_media_sessions,
|
|
parse_audio_playback,
|
|
parse_media_session,
|
|
)
|
|
|
|
FIXTURES = Path(__file__).parent / "fixtures"
|
|
|
|
|
|
def _fixture(name: str) -> str:
|
|
return (FIXTURES / name).read_text(encoding="utf-8")
|
|
|
|
|
|
class TestTiviMateHasNoSession:
|
|
"""TiviMate (ar.tvplayer.tv) registers no MediaSession on this firmware."""
|
|
|
|
def test_no_sessions_parsed(self):
|
|
assert parse_all_media_sessions(_fixture("media_session_tivimate.txt")) == []
|
|
|
|
def test_session_query_returns_none(self):
|
|
dump = _fixture("media_session_tivimate.txt")
|
|
assert parse_media_session(dump, "ar.tvplayer.tv") is None
|
|
|
|
def test_inactive_stale_session_is_not_surfaced(self):
|
|
"""Netflix registers an inactive, state-less session; the parser must
|
|
ignore it — an active=false block never appears in the results."""
|
|
dump = _fixture("media_session_tivimate.txt")
|
|
assert parse_media_session(dump, "com.netflix.ninja") is None
|
|
|
|
|
|
class TestWholphinSession:
|
|
"""Wholphin (com.github.damontecres.wholphin) publishes a proper session."""
|
|
|
|
def test_playing_session_parsed(self):
|
|
sessions = parse_all_media_sessions(_fixture("media_session_wholphin.txt"))
|
|
assert len(sessions) == 1
|
|
session = sessions[0]
|
|
assert session.package == "com.github.damontecres.wholphin"
|
|
assert session.playing is True
|
|
assert session.paused is False
|
|
assert session.title == "Twirlywoos"
|
|
assert session.artist is None
|
|
|
|
def test_foreground_package_is_preferred(self):
|
|
dump = _fixture("media_session_wholphin.txt")
|
|
best = parse_media_session(dump, "com.github.damontecres.wholphin")
|
|
assert best is not None
|
|
assert best.package == "com.github.damontecres.wholphin"
|
|
assert best.playing is True
|
|
|
|
|
|
class TestPlaybackStatePosition:
|
|
"""PlaybackState carries position (ms), speed, and uptime-ms `updated` (R9)."""
|
|
|
|
def test_wholphin_position_fields_parsed(self):
|
|
sessions = parse_all_media_sessions(_fixture("media_session_wholphin.txt"))
|
|
assert len(sessions) == 1
|
|
session = sessions[0]
|
|
assert session.position == 17237
|
|
assert session.speed == 1.0
|
|
assert session.updated == 1408392
|
|
|
|
def test_buffered_position_is_not_mistaken_for_position(self):
|
|
"""`buffered position=70070` sits in the same block; only the real
|
|
`position=17237` may be read."""
|
|
sessions = parse_all_media_sessions(_fixture("media_session_wholphin.txt"))
|
|
assert sessions[0].position != 70070
|
|
|
|
def test_position_at_uptime_extrapolates_with_speed(self):
|
|
session = MediaSessionInfo(
|
|
position=17237, speed=1.0, updated=1408392, playing=True
|
|
)
|
|
assert session.position_at_uptime(1408392 + 1000) == 18237
|
|
assert session.position_at_uptime(1408392) == 17237
|
|
|
|
def test_position_at_uptime_none_without_sample(self):
|
|
assert MediaSessionInfo().position_at_uptime(1_000_000) is None
|
|
|
|
def test_paused_session_parses_state_and_position(self):
|
|
dump = (
|
|
"Sessions Stack - have 1 sessions:\n"
|
|
" Some app session com.example.player/Session (userId=0)\n"
|
|
" package=com.example.player\n"
|
|
" active=true\n"
|
|
" state=PlaybackState {state=2, position=4200, buffered position=9000, "
|
|
"speed=0.0, updated=500, actions=1, custom actions=[], active item id=0, "
|
|
"error=null}\n"
|
|
" metadata: size=2, description=Song Title, Some Artist\n"
|
|
" Audio playback (lastly played comes first)\n"
|
|
" uid=10100 packages=com.example.player\n"
|
|
)
|
|
sessions = parse_all_media_sessions(dump)
|
|
assert len(sessions) == 1
|
|
session = sessions[0]
|
|
assert session.paused is True
|
|
assert session.playing is False
|
|
assert session.position == 4200
|
|
assert session.speed == 0.0
|
|
assert session.updated == 500
|
|
assert session.title == "Song Title"
|
|
assert session.artist == "Some Artist"
|
|
|
|
def test_stateless_playbackstate_line_ignored(self):
|
|
"""A non-PlaybackState `state=` line must not crash or fabricate state."""
|
|
dump = (
|
|
" Some app session com.example.player/Session (userId=0)\n"
|
|
" package=com.example.player\n"
|
|
" active=true\n"
|
|
" state=null\n"
|
|
" metadata: size=1, description=Only Title\n"
|
|
)
|
|
sessions = parse_all_media_sessions(dump)
|
|
assert len(sessions) == 1
|
|
assert sessions[0].playing is False
|
|
assert sessions[0].paused is False
|
|
assert sessions[0].position is None
|
|
|
|
|
|
class TestAudioPlaybackSection:
|
|
"""The "Audio playback (lastly played comes first)" section lists apps
|
|
that recently played audio even with no MediaSession (R2)."""
|
|
|
|
def test_tivimate_shows_up_without_a_session(self):
|
|
packages = parse_audio_playback(_fixture("media_session_tivimate.txt"))
|
|
assert packages == [
|
|
"ar.tvplayer.tv",
|
|
"com.google.android.apps.mediashell",
|
|
]
|
|
|
|
def test_order_is_dump_order_most_recent_first(self):
|
|
packages = parse_audio_playback(_fixture("media_session_tivimate.txt"))
|
|
assert packages[0] == "ar.tvplayer.tv"
|
|
|
|
def test_wholphin_audio_playback(self):
|
|
assert parse_audio_playback(_fixture("media_session_wholphin.txt")) == [
|
|
"com.github.damontecres.wholphin"
|
|
]
|
|
|
|
def test_lines_before_the_section_are_ignored(self):
|
|
"""`... package:` listener lines precede the section and must not leak
|
|
into the audio-playback package list."""
|
|
dump = (
|
|
" Media key listener package:\n"
|
|
" OnMediaKeyEventDispatchedListener: added 0 listener(s)\n"
|
|
" Audio playback (lastly played comes first)\n"
|
|
" uid=10117 packages=ar.tvplayer.tv\n"
|
|
)
|
|
assert parse_audio_playback(dump) == ["ar.tvplayer.tv"]
|
|
|
|
def test_empty_dump(self):
|
|
assert parse_audio_playback("") == []
|
|
|
|
|
|
def test_running_apps_last_known_enrichment():
|
|
"""P3-3b: apps without a live session carry their TTL-capped last-known state."""
|
|
from nsh.ha_adb_common import MediaSessionInfo, running_apps_from_tasks
|
|
|
|
tasks = [
|
|
{"package": "ar.tvplayer.tv"},
|
|
{"package": "com.netflix.ninja"},
|
|
]
|
|
sessions = [MediaSessionInfo(package="com.netflix.ninja", playing=True, title="Movie")]
|
|
last_known = {
|
|
"ar.tvplayer.tv": MediaSessionInfo(
|
|
package="ar.tvplayer.tv", playing=False, paused=True, title="Last show"
|
|
)
|
|
}
|
|
|
|
apps = running_apps_from_tasks(tasks, sessions, foreground_package=None, last_known=last_known)
|
|
|
|
tivi = next(a for a in apps if a["package"] == "ar.tvplayer.tv")
|
|
netflix = next(a for a in apps if a["package"] == "com.netflix.ninja")
|
|
|
|
assert tivi["state"] == "running" # no live session
|
|
assert tivi["last_known_state"] == "paused"
|
|
assert tivi["last_known_title"] == "Last show"
|
|
assert "last_known_state" not in netflix # live session enriched instead
|
|
assert netflix["state"] == "playing"
|