Files
matrix-screen-controller/移动端相关内容/安卓app/如何安卓测试/run_emulator_ui_test.py
T

153 lines
8.7 KiB
Python

"""Run selected isolated Compose UI tests on an explicitly named Android Studio AVD."""
import argparse
import json
from pathlib import Path
import re
import subprocess
import time
SCENARIOS = ('connection', 'wifiTransport', 'status', 'content', 'settings', 'navigation', 'layout')
CONFIGS = {'baseline': (None, '1.0', '0'), 'narrow': (340, '1.0', '0'),
'large-font': (340, '1.3', '0'), 'landscape': (None, '1.3', '1')}
PACKAGE = 'org.qimiaoscreen.uitest'
def passed(output):
return bool(re.search(r'^OK \(1 test\)\s*$', output, re.M)) and not any(
marker in output for marker in ('FAILURES!!!', 'INSTRUMENTATION_FAILED', 'Process crashed'))
def main(argv=None):
p = argparse.ArgumentParser(description=__doc__)
p.add_argument('--list-scenarios', action='store_true')
p.add_argument('--dry-run', action='store_true')
choice = p.add_mutually_exclusive_group()
choice.add_argument('--scenario', action='append', choices=SCENARIOS)
choice.add_argument('--all-ui', action='store_true')
p.add_argument('--configuration', action='append', choices=CONFIGS)
p.add_argument('--avd')
p.add_argument('--adb')
p.add_argument('--emulator')
p.add_argument('--sysdir', type=Path, help='Optional system image directory for a separately stored SDK image')
p.add_argument('--port', type=int, default=5580)
p.add_argument('--build', type=Path)
p.add_argument('--report', type=Path)
args = p.parse_args(argv)
if args.list_scenarios:
print(json.dumps({'scenarios': SCENARIOS, 'configurations': list(CONFIGS)})); return 0
selected = list(SCENARIOS) if args.all_ui else list(dict.fromkeys(args.scenario or []))
if not selected: p.print_help(); return 2
configs = list(dict.fromkeys(args.configuration or ['baseline']))
plan = [{'scenario': s, 'configuration': c, 'result': 'not run'} for c in configs for s in selected]
print(json.dumps({'selected': plan, 'physical_phone': False, 'board': False, 'root': False}), flush=True)
if args.dry_run: return 0
if not args.adb or not args.avd or not args.build: p.error('--adb, --avd and --build required')
if not re.fullmatch(r'[A-Za-z0-9_.-]+', args.avd): p.error('Invalid AVD name')
if args.port % 2 or not 5554 <= args.port <= 5682: p.error('Expected an even emulator console port')
apk = args.build / 'uiTest/build/outputs/apk/debug/uiTest-debug.apk'
if not apk.is_file(): p.error('Build :uiTest:assembleDebug first')
if args.report and args.report.exists(): p.error('Report already exists; choose a new path')
serial = 'emulator-' + str(args.port)
started = False
verified_target = False
original = {}
cleanup_errors = []
process = None
def adb(*command, timeout=30):
try:
result = subprocess.run([args.adb, '-s', serial, *command], capture_output=True,
text=True, encoding='utf-8', errors='replace', timeout=timeout)
except subprocess.TimeoutExpired:
raise RuntimeError('Emulator operation timed out') from None
if result.returncode: raise RuntimeError('Emulator operation failed')
return result.stdout + result.stderr
def cleanup(name, action):
try: action()
except Exception: cleanup_errors.append(name)
def restore_setting(key, value):
adb('shell', 'settings', 'delete', 'system', key) if value == 'null' else adb('shell', 'settings', 'put', 'system', key, value)
if adb('shell', 'settings', 'get', 'system', key).strip() != value: raise RuntimeError('Restore failed')
failure = None
try:
# An occupied port must not cause selection of another device.
state = subprocess.run([args.adb, '-s', serial, 'get-state'], capture_output=True, timeout=15)
if state.returncode:
if not args.emulator: raise RuntimeError('AVD is not running; supply --emulator to start it')
available = subprocess.run([args.emulator, '-list-avds'], capture_output=True, text=True, timeout=20)
if args.avd not in available.stdout.splitlines(): raise RuntimeError('Named AVD does not exist; create it in Android Studio Device Manager')
command = [args.emulator, '-avd', args.avd, '-port', str(args.port), '-no-window', '-no-audio', '-no-snapshot', '-no-boot-anim']
if args.sysdir: command += ['-sysdir', str(args.sysdir)]
process = subprocess.Popen(command, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
creationflags=getattr(subprocess, 'CREATE_NO_WINDOW', 0))
started = True
deadline = time.monotonic() + 180
while True:
if process is not None and process.poll() is not None: raise RuntimeError('Emulator exited before boot')
try:
if adb('shell', 'getprop', 'sys.boot_completed').strip() == '1': break
except RuntimeError: pass
if time.monotonic() >= deadline: raise RuntimeError('AVD boot timeout')
time.sleep(2)
if adb('shell', 'getprop', 'ro.kernel.qemu').strip() != '1': raise RuntimeError('Refusing a physical device')
if adb('emu', 'avd', 'name').splitlines()[0].strip() != args.avd: raise RuntimeError('AVD name does not match requested target')
verified_target = True
original = {k: adb('shell', 'settings', 'get', 'system', k).strip()
for k in ('font_scale', 'accelerometer_rotation', 'user_rotation')}
density = re.search(r'Override density: (\d+)', adb('shell', 'wm', 'density'))
original['density'] = density[1] if density else 'reset'
dims = re.findall(r'(\d+)x(\d+)', adb('shell', 'wm', 'size'))[-1]
result = adb('install', '-r', str(apk), timeout=120)
if 'Success' not in result: raise RuntimeError('Test APK installation failed')
for item in plan:
name, config = item['scenario'], item['configuration']
width, font, rotation = CONFIGS[config]
adb('shell', 'am', 'force-stop', PACKAGE)
adb('shell', 'wm', 'density', str(min(map(int, dims)) * 160 // width) if width else original['density'])
for key, value in [('font_scale', font), ('accelerometer_rotation', '0'), ('user_rotation', rotation)]:
adb('shell', 'settings', 'put', 'system', key, value)
try:
output = adb('shell', 'am', 'instrument', '-w', '-r', '-e', 'class',
PACKAGE + '.IsolatedUiTest#' + name,
PACKAGE + '/androidx.test.runner.AndroidJUnitRunner', timeout=120)
if not passed(output):
# Isolated fixtures contain no private data. Keep assertion diagnostics for repair.
item["diagnostic"] = output[:2500]
print(output[:2500] + ("\n[remaining diagnostics omitted]" if len(output) > 2500 else ""), flush=True)
raise RuntimeError('Selected Compose scenario failed')
item['result'] = 'passed'
except Exception:
item['result'] = 'failed'; raise
print(json.dumps(item), flush=True)
except Exception as e:
failure = str(e)
finally:
if original:
cleanup('stop-test', lambda: adb('shell', 'am', 'force-stop', PACKAGE))
for key, value in original.items():
if key != 'density': cleanup(key, lambda k=key, v=value: restore_setting(k, v))
def density_restore():
adb('shell', 'wm', 'density', original['density'])
current = re.search(r'Override density: (\d+)', adb('shell', 'wm', 'density'))
if (current[1] if current else 'reset') != original['density']: raise RuntimeError('Density restore failed')
cleanup('density', density_restore)
if started and process is not None and process.poll() is None:
if verified_target:
cleanup('stop-owned-emulator', lambda: adb('emu', 'kill'))
else:
process.terminate()
if process is not None:
try: process.wait(timeout=20)
except subprocess.TimeoutExpired:
process.terminate(); cleanup_errors.append('emulator-exit')
report = {'tests': plan, 'failure': failure, 'cleanup_errors': cleanup_errors,
'not_selected': [s for s in SCENARIOS if s not in selected]}
if args.report:
args.report.parent.mkdir(parents=True, exist_ok=True)
with args.report.open('x', encoding='utf-8') as f: json.dump(report, f, ensure_ascii=False, indent=2)
print(json.dumps(report, ensure_ascii=False), flush=True)
return 1 if failure or cleanup_errors else 0
if __name__ == '__main__':
raise SystemExit(main())