diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..55401ec --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,12 @@ +"""Test bootstrap. + +Puts the vendored ha_adb_common package on sys.path so helpers can be imported +directly (their code is stdlib-only) without installing Home Assistant or +executing the integration's __init__ module. +""" + +import sys +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parent.parent +sys.path.insert(0, str(REPO_ROOT / "custom_components" / "nvidia_shield")) diff --git a/tests/fixtures/activity_activities_tivimate.txt b/tests/fixtures/activity_activities_tivimate.txt new file mode 100644 index 0000000..cac2503 --- /dev/null +++ b/tests/fixtures/activity_activities_tivimate.txt @@ -0,0 +1,5 @@ +mResumedActivity: ActivityRecord{4f0d030 u0 ar.tvplayer.tv/.ui.MainActivity t8152} + state=RESUMED stopped=false delayedResume=false finishing=false + Resumed activities in task display areas (from top to bottom): + Resumed: ActivityRecord{4f0d030 u0 ar.tvplayer.tv/.ui.MainActivity t8152} + ResumedActivity: ActivityRecord{4f0d030 u0 ar.tvplayer.tv/.ui.MainActivity t8152} diff --git a/tests/fixtures/media_session_tivimate.txt b/tests/fixtures/media_session_tivimate.txt new file mode 100644 index 0000000..aa507a4 --- /dev/null +++ b/tests/fixtures/media_session_tivimate.txt @@ -0,0 +1,33 @@ +MEDIA SESSION SERVICE (dumpsys media_session) + + 9 sessions listeners. + Global priority session is null + User Records: + Record for full_user=0 + Volume key long-press listener: null + Volume key long-press listener package: + Media key listener: null + Media key listener package: + OnMediaKeyEventDispatchedListener: added 0 listener(s) + OnMediaKeyEventSessionChangedListener: added 1 listener(s) + from com.android.bluetooth + Last MediaButtonReceiver: null + Media button session is null + Sessions Stack - have 1 sessions: + Netflix media session com.netflix.ninja/Netflix media session (userId=0) + ownerPid=5833, ownerUid=10080, userId=0 + package=com.netflix.ninja + launchIntent=null + mediaButtonReceiver=null + active=false + flags=3 + rating type=0 + controllers: 0 + state=null + audioAttrs=AudioAttributes: usage=USAGE_MEDIA content=CONTENT_TYPE_UNKNOWN flags=0x800 tags= bundle=null + volumeType=1, controlType=2, max=0, current=0 + metadata: null + queueTitle=null, size=0 + Audio playback (lastly played comes first) + uid=10117 packages=ar.tvplayer.tv + uid=10006 packages=com.google.android.apps.mediashell diff --git a/tests/fixtures/media_session_wholphin.txt b/tests/fixtures/media_session_wholphin.txt new file mode 100644 index 0000000..b121f86 --- /dev/null +++ b/tests/fixtures/media_session_wholphin.txt @@ -0,0 +1,23 @@ +MEDIA SESSION SERVICE (dumpsys media_session) + + 9 sessions listeners. + User Records: + Record for full_user=0 + Media button session is null + Sessions Stack - have 1 sessions: + Wholphin media session com.github.damontecres.wholphin/Wholphin media session (userId=0) + ownerPid=6117, ownerUid=10117, userId=0 + package=com.github.damontecres.wholphin + launchIntent=null + mediaButtonReceiver=null + active=true + flags=3 + rating type=0 + controllers: 1 + state=PlaybackState {state=3, position=17237, buffered position=70070, speed=1.0, updated=1408392, actions=7339995, custom actions=[], active item id=0, error=null} + audioAttrs=AudioAttributes: usage=USAGE_MEDIA content=CONTENT_TYPE_MOVIE flags=0x800 tags= bundle=null + volumeType=1, controlType=2, max=0, current=0 + metadata: size=11, description=Twirlywoos, null, null + queueTitle=null, size=1 + Audio playback (lastly played comes first) + uid=10117 packages=com.github.damontecres.wholphin diff --git a/tests/test_intent.py b/tests/test_intent.py new file mode 100644 index 0000000..1b3bf67 --- /dev/null +++ b/tests/test_intent.py @@ -0,0 +1,32 @@ +"""Tests for the PlaybackIntent dataclass (optimistic state tracking).""" + +from ha_adb_common.intent import PlaybackIntent + + +def test_create_captures_fields(): + intent = PlaybackIntent.create( + target_state="paused", command="media_pause", prior_state="playing" + ) + assert intent.target_state == "paused" + assert intent.command == "media_pause" + assert intent.prior_state == "playing" + assert intent.retry_count == 0 + assert intent.created_at > 0 + + +def test_debug_dict_shape(): + intent = PlaybackIntent.create(target_state="playing", command="media_play") + debug = intent.to_debug_dict() + assert debug["target"] == "playing" + assert debug["command"] == "media_play" + assert debug["retries"] == 0 + assert "age_s" in debug + + +def test_age_increases(): + import time + + intent = PlaybackIntent.create(target_state="playing", command="media_play") + before = intent.age_seconds + time.sleep(0.01) + assert intent.age_seconds >= before diff --git a/tests/test_media_session.py b/tests/test_media_session.py new file mode 100644 index 0000000..40068cc --- /dev/null +++ b/tests/test_media_session.py @@ -0,0 +1,53 @@ +"""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 parse_all_media_sessions, 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 diff --git a/tests/test_running_apps.py b/tests/test_running_apps.py new file mode 100644 index 0000000..620ae6d --- /dev/null +++ b/tests/test_running_apps.py @@ -0,0 +1,40 @@ +"""Tests for the running-apps list builder (recent tasks + media sessions).""" + +from ha_adb_common.running_apps import running_apps_from_tasks + + +def _session(package: str, playing: bool = False, title: str | None = None): + from ha_adb_common.media_session import MediaSessionInfo + + s = MediaSessionInfo(package=package) + s.playing = playing + s.paused = not playing + s.title = title + return s + + +def test_returns_none_for_fewer_than_two_tasks(): + assert running_apps_from_tasks([]) is None + assert running_apps_from_tasks([{"package": "app.a"}]) is None + + +def test_marks_foreground_and_defaults_to_running(): + tasks = [{"package": "app.a"}, {"package": "app.b"}] + apps = running_apps_from_tasks(tasks, [], foreground_package="app.b") + assert apps is not None + assert len(apps) == 2 + by_pkg = {a["package"]: a for a in apps} + assert "foreground" not in by_pkg["app.a"] + assert by_pkg["app.b"]["foreground"] is True + assert by_pkg["app.a"]["state"] == "running" + + +def test_enriches_with_media_session_state(): + tasks = [{"package": "app.a"}, {"package": "app.b"}] + sessions = [_session("app.a", playing=True, title="Song")] + apps = running_apps_from_tasks(tasks, sessions) + assert apps is not None + by_pkg = {a["package"]: a for a in apps} + assert by_pkg["app.a"]["state"] == "playing" + assert by_pkg["app.a"]["title"] == "Song" + assert by_pkg["app.b"]["state"] == "running"