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

226 lines
15 KiB
Python

"""Run registered BLE tests; WiFi reapplication requires its explicit option."""
import argparse
import json
from pathlib import Path
import subprocess
import sys
import time
sys.path.insert(0, str(Path(__file__).resolve().parent / '测试设备登记'))
from prepare_test_device import load_registration, check_connected
def main(argv=None):
parser = argparse.ArgumentParser()
parser.add_argument('--scenario', required=True)
parser.add_argument('--adb', required=True)
parser.add_argument('--build', type=Path, required=True)
parser.add_argument('--poll-cycles', type=int, default=60)
parser.add_argument('--test', choices=('ble', 'activity', 'layout'), default='ble')
parser.add_argument('--root-input', action='store_true', help='Compatibility flag; activity tests always use authorized root input')
parser.add_argument('--reapply-current-wifi', action='store_true', help='Explicitly reapply the already active saved WiFi profile over BLE; may briefly reconnect WiFi')
parser.add_argument('--verify-current-controls', action='store_true', help='Reapply current brightness; briefly pause active animation and restore without selecting new content')
parser.add_argument('--toggle-bluetooth', action='store_true', help='Requires explicit root Bluetooth-switch authorization plus activity root input')
parser.add_argument('--grant-root', action='store_true', help='Compatibility flag; runner always verifies and grants authorized App permissions')
args = parser.parse_args(argv)
from run_ble_test import SCENARIOS
from run_emulator_ui_test import passed
if args.scenario not in SCENARIOS or args.test != SCENARIOS[args.scenario][0]:
parser.error('Scenario and instrumentation family do not match')
if args.reapply_current_wifi != (args.scenario == 'wifi-reapply') or args.verify_current_controls != (args.scenario == 'controls') or args.toggle_bluetooth != (args.scenario == 'bluetooth-toggle'):
parser.error('Mutation flags must match the explicit scenario')
if args.poll_cycles and args.scenario != 'polling': parser.error('Polling requires polling scenario')
args.grant_root = True
if args.test in ("activity", "layout"): args.root_input = True
if not 0 <= args.poll_cycles <= 60:
parser.error('poll-cycles must be between 0 and 60')
if args.root_input and args.test == 'ble':
parser.error('root-input only applies to activity tests')
if args.reapply_current_wifi and args.test != 'ble':
parser.error('reapply-current-wifi only applies to BLE tests')
if args.verify_current_controls and args.test != 'ble':
parser.error('verify-current-controls only applies to BLE tests')
if args.toggle_bluetooth and not (args.test == 'activity' and args.root_input):
parser.error('toggle-bluetooth requires activity and root-input')
device = next(d for d in load_registration() if d['alias'] == 'android-test-a')
check_connected(args.adb, device)
root_check = subprocess.run([args.adb, '-s', device['serial'], 'shell', 'su', '-c', 'id -u'], capture_output=True, text=True, timeout=15)
if root_check.returncode or root_check.stdout.strip() != '0':
raise RuntimeError('测试手机 root 权限不可用;停止测试,等待用户处理,不使用备用注入方式')
board_id = board_name = library_hash = ""
if SCENARIOS[args.scenario][1]:
from remote_support import connect, execute
client, fields = connect()
try:
code, _, _ = execute(client, "command -v python3 >/dev/null && python3 --version", password=fields['密码'])
if code: raise RuntimeError('Remote Python unavailable')
code, output, error = execute(client, "python3 -c \"import json; p=json.load(open('/var/lib/matrix-screen-controller/mobile/identity.json')); print(json.dumps({'device_id':p['device_id']}))\"", password=fields['密码'])
if code:
raise RuntimeError('Cannot read authorized board identity')
board_id = json.loads(output)['device_id']
from uuid import UUID
UUID(board_id)
board_name = 'QMS-' + board_id.replace('-', '')[:8]
library_hash = ""
if args.scenario == "library-order":
code, output, _ = execute(client, "python3 -c \"import urllib.request,json,hashlib; d=json.load(urllib.request.urlopen('http://127.0.0.1:8080/api/library/order',timeout=5)); print(hashlib.sha256('\\n'.join(i['type']+'|'+i['id'] for i in d['items']).encode()).hexdigest())\"", password=fields['密码'])
library_hash = output.strip()
if code or len(library_hash) != 64 or any(c not in '0123456789abcdef' for c in library_hash):
raise RuntimeError('Cannot read device library order digest')
if not board_name.startswith('QMS-') or len(board_name) != 12:
raise RuntimeError('Invalid board identity')
finally:
client.close()
def adb(*command, timeout=120, quiet=False):
try:
result = subprocess.run([args.adb, '-s', device['serial'], *command], capture_output=True,
text=True, encoding='utf-8', errors='replace', timeout=timeout)
except subprocess.TimeoutExpired:
# ADB's host timeout does not stop Android instrumentation itself.
if command[:3] == ('shell', 'am', 'instrument'):
subprocess.run([args.adb, '-s', device['serial'], 'shell', 'am', 'force-stop',
'org.qimiaoscreen.controller'], capture_output=True, timeout=15)
raise RuntimeError('ADB operation timed out; device identifiers omitted') from None
output = result.stdout + result.stderr
for secret, alias in ((device['serial'], '[test-phone]'), (board_name, '[test-board]'), (board_id, '[test-board-id]')):
if secret: output = output.replace(secret, alias)
if not quiet:
print(output, flush=True)
if result.returncode:
raise RuntimeError('ADB command failed')
return output
api = int(adb('shell', 'getprop', 'ro.build.version.sdk', quiet=True).strip())
permissions = ('BLUETOOTH_SCAN', 'BLUETOOTH_CONNECT') if api >= 31 else ('ACCESS_FINE_LOCATION',)
if api >= 37 and args.scenario.startswith('wifi-transport'):
permissions += ('ACCESS_LOCAL_NETWORK',)
permission_original = {}
if args.scenario == 'navigation':
existing = adb('shell', 'dumpsys', 'package', 'org.qimiaoscreen.controller', quiet=True)
permission_original = {p: 'android.permission.' + p + ': granted=true' in existing for p in permissions}
layout_original = None
layout_density = None
restart_probe = None
try:
for relative in ('androidApp/build/outputs/apk/debug/androidApp-debug.apk',
'androidApp/build/outputs/apk/androidTest/debug/androidApp-debug-androidTest.apk'):
apk = args.build / relative
if not apk.is_file():
raise RuntimeError('APK missing')
if 'Success' not in adb('install', '-r', str(apk)):
raise RuntimeError('APK installation did not succeed')
adb('shell', 'am', 'force-stop', 'org.qimiaoscreen.controller')
if args.grant_root and args.scenario != 'navigation':
# Some vendor package managers asynchronously reset permissions after
# install succeeds. Verify after a settling window, not just at grant.
for attempt in range(3):
for permission in permissions:
adb('shell', 'su', '-c', 'pm grant org.qimiaoscreen.controller android.permission.' + permission)
time.sleep(3)
verified = adb('shell', 'dumpsys', 'package', 'org.qimiaoscreen.controller', quiet=True)
if all('android.permission.' + p + ': granted=true' in verified for p in permissions):
break
else:
raise RuntimeError('Bluetooth permission did not remain granted after installation')
if args.scenario == 'navigation':
for permission in permissions:
adb('shell', 'su', '-c', 'pm revoke org.qimiaoscreen.controller android.permission.' + permission, quiet=True)
current = adb('shell', 'dumpsys', 'package', 'org.qimiaoscreen.controller', quiet=True)
if any('android.permission.' + p + ': granted=true' in current for p in permissions):
raise RuntimeError('Navigation must not retain permission to auto-connect')
if args.root_input:
adb('push', str(Path(__file__).with_name('authorized_app_input.sh')), '/data/local/tmp/qms-authorized-input.sh', quiet=True)
if args.scenario.startswith('wifi-transport'):
if 'wifi is enabled' not in adb('shell', 'cmd', 'wifi', 'status', quiet=True).lower().replace('-', ''):
raise RuntimeError('WiFi transport scenario requires initially enabled WiFi')
adb('push', str(Path(__file__).with_name('authorized_root_command.sh')), '/data/local/tmp/qms-authorized-root.sh', quiet=True)
if args.scenario in ("connection-entry", "wifi-connection-entry"):
adb("push", str(Path(__file__).with_name("authorized_root_command.sh")), "/data/local/tmp/qms-authorized-root.sh", quiet=True)
if args.scenario == "layout":
adb("push", str(Path(__file__).with_name("authorized_root_command.sh")), "/data/local/tmp/qms-authorized-root.sh", quiet=True)
def root(command):
return adb("shell", "su", "-c", command, quiet=True).strip()
import re
layout_original = {key: root("settings get system " + key) for key in ("font_scale", "accelerometer_rotation", "user_rotation")}
layout_density = re.search(r"Override density: (\d+)", root("wm density"))
layout_density = layout_density[1] if layout_density else "reset"
dimensions = re.findall(r"(\d+)x(\d+)", root("wm size"))[-1]
narrow = min(map(int, dimensions)) * 160 // 340
for font, rotation in (("1.0", "0"), ("1.3", "0"), ("1.3", "1")) if args.scenario == "layout" else ((None, None),):
if layout_original:
root("am force-stop org.qimiaoscreen.controller")
root("wm density " + str(narrow))
root("settings put system font_scale " + font)
root("settings put system accelerometer_rotation 0")
root("settings put system user_rotation " + rotation)
root("input keyevent KEYCODE_WAKEUP")
root("wm dismiss-keyguard")
root("am start -W -n org.qimiaoscreen.controller/.MainActivity")
board_args = ['-e', 'device_name', board_name] if board_name else []
if library_hash: board_args += ['-e', 'library_order_hash', library_hash]
if args.scenario == 'wifi-transport-restart':
from wifi_restart_probe import RestartProbe
restart_probe = RestartProbe()
restart_probe.start()
output = adb('shell', 'am', 'instrument', '-w', '-r', *board_args,
'-e', 'scenario', args.scenario,
'-e', 'poll_cycles', str(args.poll_cycles),
'-e', 'root_input', str(args.root_input).lower(),
'-e', 'reapply_current_wifi', str(args.reapply_current_wifi).lower(),
'-e', 'verify_current_controls', str(args.verify_current_controls).lower(),
'-e', 'toggle_bluetooth', str(args.toggle_bluetooth).lower(),
'-e', 'class', 'org.qimiaoscreen.controller.' + ('WifiTransportTest' if args.scenario.startswith('wifi-transport') else 'LibraryCacheTest' if args.scenario == 'cache-storage' else
'LibraryBleCacheTest' if args.scenario == 'library-cache' else
{'ble': 'BleIntegrationTest', 'activity': 'AppLifecycleTest', 'layout': 'LayoutTest'}[args.test]),
'org.qimiaoscreen.controller.test/androidx.test.runner.AndroidJUnitRunner', timeout=360)
if not passed(output):
raise RuntimeError("Instrumentation did not pass")
if layout_original:
print("Layout passed: font=" + font + ", rotation=" + rotation)
finally:
errors = []
def cleanup(label, action):
try: action()
except Exception: errors.append(label)
cleanup('stop-test', lambda: adb('shell', 'am', 'force-stop', 'org.qimiaoscreen.controller', quiet=True))
if restart_probe is not None:
cleanup('real service restart', restart_probe.finish)
if args.scenario == 'wifi-transport-recovery':
cleanup('wifi restore', lambda: adb('shell', 'su', '-c', 'svc wifi enable', quiet=True))
cleanup('bluetooth restore', lambda: adb('shell', 'su', '-c', 'cmd bluetooth_manager enable', quiet=True))
time.sleep(3)
if 'wifi is enabled' not in adb('shell', 'cmd', 'wifi', 'status', quiet=True).lower().replace('-', ''):
errors.append('wifi restoration verification')
if layout_original and layout_density is not None:
cleanup('stop', lambda: root('am force-stop org.qimiaoscreen.controller'))
cleanup('density', lambda: root('wm density ' + layout_density))
def verify_density():
value = re.search(r'Override density: (\d+)', root('wm density'))
if (value[1] if value else 'reset') != layout_density:
raise RuntimeError('density restore failed')
cleanup('density verification', verify_density)
for key, value in layout_original.items():
def restore(key=key, value=value):
root('settings delete system ' + key if value == 'null' else 'settings put system ' + key + ' ' + value)
if root('settings get system ' + key) != value: raise RuntimeError('settings restore failed')
cleanup(key, restore)
for permission, granted in permission_original.items():
def restore_permission(permission=permission, granted=granted):
adb('shell', 'su', '-c', 'pm ' + ('grant' if granted else 'revoke') + ' org.qimiaoscreen.controller android.permission.' + permission, quiet=True)
actual = adb('shell', 'dumpsys', 'package', 'org.qimiaoscreen.controller', quiet=True)
if ('android.permission.' + permission + ': granted=true' in actual) != granted:
raise RuntimeError('Permission restore failed')
cleanup('permission ' + permission, restore_permission)
if args.toggle_bluetooth:
cleanup('bluetooth', lambda: adb('shell', 'su', '-c', 'cmd bluetooth_manager enable', quiet=True))
for helper in ('qms-authorized-root.sh', 'qms-authorized-input.sh'):
cleanup(helper, lambda helper=helper: adb('shell', 'rm', '-f', '/data/local/tmp/' + helper, quiet=True))
if errors:
raise RuntimeError('Cleanup failed: ' + ', '.join(errors))
print('Cleanup verified', flush=True)
if not passed(output):
raise RuntimeError('BLE instrumentation did not pass')
if __name__ == '__main__':
main()