R8 only ever ran on `release`, so every automated gate in this repository exercised code the shipped APK does not contain. Reflective lookups, JNI callbacks and native library loading can all break under shrinking while `flutter test`, the Robolectric suites and `connectedDebugAndroidTest` stay green — which is exactly how #1703 shipped, with the bundled FFmpeg audio renderer shrunk out of release builds for TrueHD and DTS-HD. Add a `minified` build type that inherits release's shrinker configuration but stays debuggable and debug-signed, so it is an ordinary test artifact and never a publishable one. Three integration details took a run each to find: the Flutter plugin copies app build types into every plugin module, so library-level shrinking deleted the plugin entry points that only GeneratedPluginRegistrant references; the harness must not be shrunk or the runner disappears; and androidx.test has to survive in the app under test, or the runner cannot link its own supertype and the run reports zero tests instead of failing. Instrumentation still defaults to `debug`, because only one build type can host androidTest and the existing playback suites drive media3 builder APIs the app never calls, which R8 shrinks legitimately. The new reachability test opts into the minified variant instead and touches no builder API, so the only keeps it depends on are the ones under test. Emptying proguard-rules.pro was verified to fail it.
279 lines
9.4 KiB
Python
Executable File
279 lines
9.4 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"
|
|
# Kept separate from the suites above: only one build type can host androidTest, and
|
|
# those suites drive media3 builder APIs the app itself never calls, which R8 shrinks
|
|
# legitimately. This class asserts only name-based reachability (#1703).
|
|
ANDROID_R8_REACHABILITY_CLASSES = "androidx.media3.decoder.ffmpeg.FfmpegDecoderReachabilityTest"
|
|
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's emulator routing to the 10.0.2.2 host alias is unreliable
|
|
# on this image, so reach Jellyfin over an adb reverse mapping the
|
|
# way the media suite already does.
|
|
"--adb-reverse",
|
|
"--flow",
|
|
".maestro/flows/05_playback.yaml",
|
|
"--jellyfin-log",
|
|
"build/maestro-legacy/jellyfin.log",
|
|
"--diagnostics-dir",
|
|
"build/maestro-legacy/diagnostics",
|
|
),
|
|
),
|
|
# Real Android TV hardware only, so no workflow dispatches it. The three
|
|
# `tv` regressions above run on a phone emulator that `onboard_jellyfin_tv`
|
|
# forces into TV mode; this drives the rail layout a device reports on its
|
|
# own. The D-pad-only path is also the only way to reach the TV number
|
|
# spinner, which InputModeTracker hides as soon as a tap arrives. Run as
|
|
# `python3 scripts/run_maestro_ci.py android-tv-device` with
|
|
# MAESTRO_DEVICE_ID set to the box.
|
|
"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)
|
|
# The `minified` build type runs R8 over the app under test, so a keep rule that stops
|
|
# covering a reflective lookup, a JNI callback or a native library load fails here
|
|
# instead of shipping. No other gate in this repository runs R8 at all.
|
|
#
|
|
# compileFlutterBuildMinified is deliberately not excluded: CI only prebuilds the
|
|
# debug APK, so this variant has no Flutter outputs to reuse.
|
|
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())
|