207 lines
14 KiB
Python
207 lines
14 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',)
|
|
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
|
|
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 == "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]
|
|
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.' + ('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 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()
|