Files
plezy/scripts/maestro/run_maestro_ci.py
T
edde746 908ccbb233 fix(android): retain mpv callbacks and end dismissed TV input sessions
Minified MPV initialization aborted because callback keep rules retained obsolete JNI signatures. Android TV IME dismissal also left editing active, requiring an extra Back press to leave the screen.

Keep only the named JNI callbacks without duplicating argument signatures and exercise them through the permanent minified reachability gate. Centralize native editing-session termination, observe owning-view keyboard dismissal, and reject stale completion or reopening work.

Verified the minified gate on Galaxy Z Fold3, debug MPV lifecycle and log-level suites on SHIELD and Pixel 7, native Done/Previous and two-Back behavior on SHIELD and Fire TV, 76 focused Flutter tests, analyzer parity, and shrinker checks. Both new regressions reject the original implementations.
2026-09-06 17:53:30 +02:00

269 lines
8.7 KiB
Python
Executable File

#!/usr/bin/env python3
"""Run automatic Maestro groups and guarded destructive manual targets."""
from __future__ import annotations
import argparse
import sys
from collections.abc import Sequence
import run_maestro
ANDROID_15_INSTRUMENTATION_CLASSES = (
"androidx.media3.decoder.ffmpeg.PlezyFfmpegPlaybackTest,"
"com.edde746.plezy.exoplayer.PlezyAudioModePlaybackTest"
)
ANDROID_15_INSTRUMENTATION_TARGET = "android-15-instrumentation"
# Keep reachability separate from the debug playback suites above: those exercise
# APIs the app legitimately shrinks. This target guards FFmpeg and MPV's native paths.
ANDROID_R8_REACHABILITY_CLASSES = (
"androidx.media3.decoder.ffmpeg.FfmpegDecoderReachabilityTest,"
"com.edde746.plezy.mpv.MpvPlayerReachabilityTest"
)
ANDROID_R8_REACHABILITY_TARGET = "android-r8-reachability"
GROUPS: dict[str, tuple[tuple[str, ...], ...]] = {
"android-15": (
("basic",),
("catalog",),
("media",),
(
"basic",
"--flow",
".maestro/regression_flows/03_tv_library_focus.yaml",
"--jellyfin-log",
"build/maestro-tv/library-focus.log",
"--diagnostics-dir",
"build/maestro-tv/library-focus-diagnostics",
),
(
"basic",
"--flow",
".maestro/regression_flows/04_tv_player_keys.yaml",
"--jellyfin-log",
"build/maestro-tv/player-keys.log",
"--diagnostics-dir",
"build/maestro-tv/player-keys-diagnostics",
),
(
"basic",
"--flow",
".maestro/regression_flows/05_tv_next_episode_back.yaml",
"--jellyfin-log",
"build/maestro-tv/next-episode.log",
"--diagnostics-dir",
"build/maestro-tv/next-episode-diagnostics",
),
(
"basic",
"--flow",
".maestro/regression_flows/07_sheet_back_dismiss.yaml",
"--jellyfin-log",
"build/maestro-sheets/back-dismiss.log",
"--diagnostics-dir",
"build/maestro-sheets/back-dismiss-diagnostics",
),
(
"basic",
"--flow",
".maestro/regression_flows/08_track_choice_survives_pending_pass.yaml",
"--jellyfin-log",
"build/maestro-tracks/pending-pass.log",
"--diagnostics-dir",
"build/maestro-tracks/pending-pass-diagnostics",
),
(
"basic",
"--flow",
".maestro/regression_flows/09_language_picker_locales.yaml",
"--jellyfin-log",
"build/maestro-i18n/language-picker.log",
"--diagnostics-dir",
"build/maestro-i18n/language-picker-diagnostics",
),
(
"basic",
"--fault",
"music-failure",
"--flow",
".maestro/real_flows/02_music_browse.yaml",
"--jellyfin-log",
"build/maestro-recovery/music-jellyfin.log",
"--proxy-journal",
"build/maestro-recovery/music-proxy-journal.jsonl",
"--diagnostics-dir",
"build/maestro-recovery/music-diagnostics",
),
(
"basic",
"--fault",
"offline",
"--flow",
".maestro/flows/09_download_offline_playback.yaml",
"--jellyfin-log",
"build/maestro-offline/jellyfin.log",
"--proxy-log",
"build/maestro-offline/jellyfin-proxy.log",
"--proxy-journal",
"build/maestro-offline/proxy-journal.jsonl",
"--diagnostics-dir",
"build/maestro-offline/diagnostics",
),
(
"basic",
"--fault",
"recovery",
"--flow",
".maestro/regression_flows/06_playback_recovery.yaml",
"--jellyfin-log",
"build/maestro-recovery/jellyfin.log",
"--proxy-journal",
"build/maestro-recovery/proxy-journal.jsonl",
"--diagnostics-dir",
"build/maestro-recovery/diagnostics",
),
),
"android-9": (
(
"basic",
# API 28 uses adb reverse because 10.0.2.2 routing is unreliable here.
"--adb-reverse",
"--flow",
".maestro/flows/05_playback.yaml",
"--jellyfin-log",
"build/maestro-legacy/jellyfin.log",
"--diagnostics-dir",
"build/maestro-legacy/diagnostics",
),
),
# Manual Android TV hardware target; workflow runs phone-emulator TV regressions.
"android-tv-device": (
(
"basic",
"--adb-reverse",
"--flow",
".maestro/regression_flows/10_tv_settings_navigation.yaml",
"--jellyfin-log",
"build/maestro-tv-device/settings-navigation.log",
"--diagnostics-dir",
"build/maestro-tv-device/settings-navigation-diagnostics",
),
),
}
DESTRUCTIVE_MANUAL_TARGETS: dict[str, tuple[tuple[str, ...], ...]] = {
"profile-regressions": (
(
"basic",
"--flow",
".maestro/regression_flows/01_profile_switch_isolation.yaml",
"--jellyfin-log",
"build/maestro-profile-regressions/profile-switch-isolation-jellyfin.log",
"--diagnostics-dir",
"build/maestro-profile-regressions/profile-switch-isolation-diagnostics",
),
(
"basic",
"--flow",
".maestro/regression_flows/02_profile_teardown.yaml",
"--jellyfin-log",
"build/maestro-profile-regressions/profile-teardown-jellyfin.log",
"--diagnostics-dir",
"build/maestro-profile-regressions/profile-teardown-diagnostics",
),
),
}
def run_android_15_instrumentation() -> None:
print("==> Android 15 filtered instrumentation", flush=True)
run_maestro._run_checked(
(
"android/gradlew",
"-p",
"android",
":app:connectedDebugAndroidTest",
"-x",
":app:compileFlutterBuildDebug",
f"-Pandroid.testInstrumentationRunnerArguments.class={ANDROID_15_INSTRUMENTATION_CLASSES}",
)
)
def run_android_r8_reachability() -> None:
print("==> Android R8 reachability", flush=True)
# Run minified instrumentation to catch reflective, JNI, and native-load keep
# regressions. Do not exclude the Flutter build; CI only prebuilds debug.
run_maestro._run_checked(
(
"android/gradlew",
"-p",
"android",
":app:connectedMinifiedAndroidTest",
"-Pplezy.testBuildType=minified",
f"-Pandroid.testInstrumentationRunnerArguments.class={ANDROID_R8_REACHABILITY_CLASSES}",
)
)
def run_recipes(recipes: tuple[tuple[str, ...], ...]) -> int:
failed = False
for arguments in recipes:
print(f"==> Maestro {' '.join(arguments)}", flush=True)
exit_status = run_maestro.main(arguments)
if exit_status >= 128:
return exit_status
failed = failed or exit_status != 0
return 1 if failed else 0
def run_group(name: str) -> int:
return run_recipes(GROUPS[name])
def run_target(name: str, *, disposable_emulator: bool = False) -> int:
if name in DESTRUCTIVE_MANUAL_TARGETS:
if not disposable_emulator:
print(
f"Refusing destructive manual target {name!r}: "
"re-run with --disposable-emulator only on a disposable emulator.",
file=sys.stderr,
)
return 2
print(f"==> DESTRUCTIVE manual Maestro target: {name}", flush=True)
return run_recipes(DESTRUCTIVE_MANUAL_TARGETS[name])
if name == ANDROID_15_INSTRUMENTATION_TARGET:
run_android_15_instrumentation()
return 0
if name == ANDROID_R8_REACHABILITY_TARGET:
run_android_r8_reachability()
return 0
return run_group(name)
def main(argv: Sequence[str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"target",
choices=(
*GROUPS,
ANDROID_15_INSTRUMENTATION_TARGET,
ANDROID_R8_REACHABILITY_TARGET,
*DESTRUCTIVE_MANUAL_TARGETS,
),
)
parser.add_argument(
"--disposable-emulator",
action="store_true",
help="opt in to a destructive manual target on a disposable emulator",
)
args = parser.parse_args(argv)
return run_target(args.target, disposable_emulator=args.disposable_emulator)
if __name__ == "__main__":
raise SystemExit(main())