Add authenticated WiFi transport with BLE fallback
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
# Android 可重复测试流程
|
||||
|
||||
WiFi 加速与回退专项见 [WiFi 传输测试](WiFi传输测试.md),必须显式按影响选测。
|
||||
|
||||
**先读 [按影响选测](按影响选测.md)。以下矩阵列出可选覆盖,不是每轮必跑清单;历史版本专项仅在相关影响存在时执行。**
|
||||
|
||||
本文件维护步骤与合格标准,不写一次性通过结果。每轮测试前阅读 [常见问题与排障](常见问题与排障.md),优先沿用已验证方法。结果记录到 `../测试结果归档/`。真机先检查 `测试设备登记/prepare_test_device.py`,多机精确选择,日志仅使用代号。源码与 UI 尚未实现的测试不得写成通过。
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
# WiFi 传输按影响验证
|
||||
|
||||
适用 MOBILE-WIFI-TRANSPORT/FALLBACK/SAFETY、DEVICE-MOBILE-WIFI-TRANSPORT/LIFECYCLE、WEB-MOBILE-PRESENCE 和 TEST-MOBILE-WIFI-TRANSPORT。先读按影响选测和排障指南。下列场景须显式选择,不加入普通公共回归。
|
||||
|
||||
## 本机与模拟器
|
||||
|
||||
- Python:在设备源码根执行 `python -m pytest tests/test_mobile_wifi.py tests/test_mobile_session.py tests/test_mobile_protocol.py tests/test_mobile_control.py`。覆盖一次性凭据、身份、篡改、过期、重复写入、BLE 活跃超时、WiFi 超时、WebSocket、兼容统计字段和公开向量。
|
||||
- Android:隔离构建运行 `:sharedCore:jvmTest`、`:androidApp:lintDebug`、`:androidApp:assembleDebug`、`:androidApp:assembleDebugAndroidTest`、`:uiTest:assembleDebug`。WifiCryptoTest 验证 Python 向量;HybridRpcTest 验证回退不重放写入、只读重取、网络变化、BLE 心跳和恢复协程异常收敛。
|
||||
- 模拟器 `run_emulator_ui_test.py --scenario connection --scenario wifiTransport`,选 baseline 和 narrow。使用真实 Controller/Compose 与测试专用传输,验证 WiFi/蓝牙提示、权限原因/入口和断线清理;不代表真实局域网权限或无线链路已通过。
|
||||
- API 37 运行时权限必须在对应系统验证。拒绝和撤销权限时应仍可 BLE 控制,恢复权限可再探测;低版本真机不能替代此项。
|
||||
|
||||
## 登记真机与核桃派
|
||||
|
||||
先执行凭据脚本、手机登记与 ADB/root 门禁,核对远端工具。使用 `run_ble_test.py --scenario <场景> --adb <ADB路径> --build <本轮隔离工程目录>`。不输出真实身份、SSID、地址、凭据或原始系统网络转储。
|
||||
|
||||
| 场景 | 操作及合格条件 |
|
||||
|---|---|
|
||||
| wifi-transport | BLE 加密握手、独立 WiFi 握手、相同未使用 App 缓存的缩略图比较,再由 HybridRpc 自动升级。连续帧/图片的 WiFi 计数增加,直接测试期间 BLE 字节不增加;记录驱动实际刷新率、截止丢失与错误。耗时是样本,不宣称固定加速倍数。 |
|
||||
| wifi-transport-recovery | 手机初始 WiFi 必须已启用。root 关闭 WiFi,回退 BLE 后状态可读;等待平台完成关闭,再启用并等实际 WiFi 网络出现;自动恢复 WiFi。关闭蓝牙则整体断开且不能继续 RPC;最终恢复两种无线开关并核验。 |
|
||||
| wifi-transport-network | 仅将核桃派当前已保存且正在使用的 WiFi 原样立即应用;保留密码,通过 BLE 查询任务,完成后核对配置并确认 WiFi 通信恢复。不是不同 SSID 或 DHCP 地址变化的证据。 |
|
||||
| wifi-transport-restart | 测试会话通过 BLE 临时改名为固定测试标记;宿主仅在该标记且 WiFi 已认证时执行一次真实 systemd restart。旧会话必须关闭,重新发现、BLE 握手和 WiFi 升级后服务实例不同、设备身份相同。 |
|
||||
| wifi-connection-entry | 生产 Activity 的 WiFi 文案、重扫保留、主动断开和重新连接;按实时语义位置 root 点击,不改变布局。 |
|
||||
|
||||
双手机互斥在第二台手机可用时专项验证,缺少设备记未验证。跨子网可达、不同 SSID、AP 隔离等真实拓扑须分别准备环境;单元测试与“不比较 SSID”的实现不能冒充真实拓扑验收。
|
||||
|
||||
后台生命周期保持原规则:立即停预览,30 秒关闭两通道;前台重新握手。测试前后核对配置、用户内容、服务健康与测试辅助文件清理。服务重启测试不是整机重启或 OTA 验收。普通传输修改不启动摄像头,不切换实屏测试图案。
|
||||
|
||||
## 证据
|
||||
|
||||
归档记录本轮源摘要、版本、场景、实际环境、结果、样本计数/耗时、恢复核验、失败与重测。日志仅保留固定阶段名、数字指标和测试代号;禁止把完整 status.get 或网络信息写入归档。必要未做项明确列出。
|
||||
@@ -91,12 +91,15 @@ def main(argv=None):
|
||||
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'):
|
||||
@@ -126,7 +129,11 @@ def main(argv=None):
|
||||
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":
|
||||
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)
|
||||
@@ -150,6 +157,10 @@ def main(argv=None):
|
||||
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),
|
||||
@@ -157,7 +168,7 @@ def main(argv=None):
|
||||
'-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
|
||||
'-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)
|
||||
@@ -171,6 +182,14 @@ def main(argv=None):
|
||||
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))
|
||||
|
||||
@@ -5,6 +5,10 @@ from pathlib import Path
|
||||
|
||||
# name: (instrumentation family, board required, side effect)
|
||||
SCENARIOS = {
|
||||
'wifi-transport': ('ble', True, 'encrypted WiFi upgrade, identical image transfer comparison; no mutations'),
|
||||
'wifi-transport-network': ('ble', True, 'reapply currently active board WiFi with password retained; verify hybrid routing recovery'),
|
||||
'wifi-transport-restart': ('ble', True, 'restart board main service only after authenticated WiFi readiness; verify new BLE/WiFi session'),
|
||||
'wifi-transport-recovery': ('ble', True, 'phone WiFi and Bluetooth disable/restore; verify fallback and whole-session termination'),
|
||||
'connection': ('ble', True, 'scan, handshake, disconnect and reconnect'),
|
||||
'data': ('ble', True, 'read status/settings/storage/library/frame/wifi'),
|
||||
'library-order': ('ble', True, 'read BLE pages and board order digest'),
|
||||
@@ -22,6 +26,7 @@ SCENARIOS = {
|
||||
'remember': ('activity', True, 'forget and reconnect test device'),
|
||||
'bluetooth-toggle': ('activity', True, 'disable and restore phone Bluetooth'),
|
||||
'connection-entry': ('layout', True, 'connected entry, rescan and reconnect; layout restore'),
|
||||
'wifi-connection-entry': ('layout', True, 'production WiFi indicator, root rescan/disconnect/reconnect; no layout changes'),
|
||||
'layout': ('layout', True, 'three layout configurations and frame bounds; layout restore'),
|
||||
}
|
||||
COMMON = ('connection', 'data', 'library-order', 'navigation')
|
||||
|
||||
@@ -6,7 +6,7 @@ import re
|
||||
import subprocess
|
||||
import time
|
||||
|
||||
SCENARIOS = ('connection', 'status', 'content', 'settings', 'navigation', 'layout')
|
||||
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'
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
"""Read-only deployed source and health verification; emits no network or device identity."""
|
||||
import hashlib
|
||||
import json
|
||||
import shlex
|
||||
from remote_support import ROOT, connect, execute
|
||||
|
||||
FILES = ['app/main.py', 'app/mobile/session.py', 'app/mobile/protocol.py', 'app/mobile/control.py',
|
||||
'app/mobile/wifi.py', 'app/static/index.html', 'app/static/views/device.js', 'FEATURE_UPDATED_AT']
|
||||
|
||||
|
||||
def main():
|
||||
client, fields = connect()
|
||||
try:
|
||||
code, _, _ = execute(client, 'command -v python3 >/dev/null && command -v systemctl >/dev/null')
|
||||
if code: raise RuntimeError('Remote tools unavailable')
|
||||
program = '''import hashlib,json,urllib.request
|
||||
from pathlib import Path
|
||||
names=NAMES
|
||||
digests={n:hashlib.sha256((Path('/opt/matrix-screen-controller')/n).read_bytes()).hexdigest() for n in names}
|
||||
s=json.load(urllib.request.urlopen('http://127.0.0.1:8080/api/status',timeout=10))
|
||||
d=s['screen']['driver_status']
|
||||
print(json.dumps({'digests':digests,'health':{'mobile_available':s['mobile']['available'],
|
||||
'active_transport':s['mobile'].get('active_transport'),'driver_available':s['screen']['driver_available'],
|
||||
'driver_running':d.get('running'),'actual_refresh_rate_hz':d.get('actual_refresh_rate_hz'),
|
||||
'deadline_misses':d.get('deadline_misses'),'driver_error_absent':not d.get('last_error'),
|
||||
'feature_updated_at':s['service']['feature_updated_at'],'software_version':s['service']['software_version']}}))
|
||||
'''.replace('NAMES', repr(FILES))
|
||||
code, output, _ = execute(client, 'python3 -c ' + shlex.quote(program), password=fields['密码'])
|
||||
if code: raise RuntimeError('Deployment verification failed')
|
||||
result = json.loads(output)
|
||||
expected = {n: hashlib.sha256((ROOT / '核桃派软件源代码' / n).read_bytes()).hexdigest() for n in FILES}
|
||||
if result['digests'] != expected: raise RuntimeError('Deployed source differs from current source')
|
||||
code, _, _ = execute(client, 'systemctl is-active --quiet matrix-screen-controller.service')
|
||||
if code: raise RuntimeError('Main service is not active')
|
||||
print(json.dumps(dict(source_matches=True, service_active=True, **result['health'])))
|
||||
finally:
|
||||
client.close()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,45 @@
|
||||
"""Explicit real-system restart test, synchronized by a transient test session name."""
|
||||
import json
|
||||
import threading
|
||||
import time
|
||||
|
||||
from remote_support import connect, execute
|
||||
|
||||
|
||||
class RestartProbe:
|
||||
def __init__(self):
|
||||
self.stop = threading.Event()
|
||||
self.error = None
|
||||
self.completed = False
|
||||
self.thread = threading.Thread(target=self.run, daemon=True)
|
||||
|
||||
def start(self):
|
||||
self.thread.start()
|
||||
|
||||
def run(self):
|
||||
client = None
|
||||
try:
|
||||
client, fields = connect()
|
||||
code, _, _ = execute(client, 'command -v python3 >/dev/null && command -v systemctl >/dev/null')
|
||||
if code: raise RuntimeError('Required remote tools unavailable')
|
||||
deadline = time.monotonic() + 120
|
||||
while not self.stop.wait(1):
|
||||
if time.monotonic() >= deadline:
|
||||
raise RuntimeError('Authenticated restart readiness was not observed')
|
||||
code, output, _ = execute(client, "python3 -c \"import urllib.request,json; s=json.load(urllib.request.urlopen('http://127.0.0.1:8080/api/status',timeout=5)); m=s.get('mobile',{}); print(json.dumps({'ready':m.get('connected') and m.get('active_transport')=='wifi' and m.get('client_name')=='android-test-restart-ready'}))\"")
|
||||
if code or not json.loads(output).get('ready'):
|
||||
continue
|
||||
code, _, _ = execute(client, 'systemctl restart matrix-screen-controller.service && systemctl is-active --quiet matrix-screen-controller.service', password=fields['密码'], timeout=60)
|
||||
if code: raise RuntimeError('Real service restart failed')
|
||||
self.completed = True
|
||||
return
|
||||
except Exception:
|
||||
self.error = 'Real service restart coordination failed; no private diagnostics emitted'
|
||||
finally:
|
||||
if client is not None: client.close()
|
||||
|
||||
def finish(self):
|
||||
self.stop.set()
|
||||
self.thread.join(timeout=65)
|
||||
if self.thread.is_alive() or self.error or not self.completed:
|
||||
raise RuntimeError(self.error or 'Real service restart did not complete')
|
||||
@@ -1,5 +1,14 @@
|
||||
# Android 测试常见问题与排障
|
||||
|
||||
## WiFi 传输经验
|
||||
|
||||
- 原 `mobile.transport` 是 BLE 计数对象,不能改为字符串。实际业务链路使用新增 `mobile.active_transport`;以 BluezRuntime 组合后的状态测试,避免只测试 SessionManager 漏掉字段覆盖。
|
||||
- `cmd wifi status` 的启用文字可能为 `Wifi` 或 `Wi-Fi`,只在内存中忽略大小写与连字符后判断,不输出包含 SSID/IP 的原始内容。
|
||||
- `invokeOnCompletion` 不是协程异常处理器。网络监听的 collectLatest 内恢复屏障也可能因 BLE 已断开而失败;必须在 launch 主体捕获并结束会话,不能仅在完成回调做清理,否则可能断开成功后仍崩溃。
|
||||
- 网络回调取消不能截断正在发出的加密 BLE 消息。通道协商/撤销须有界地完成 BLE 交换,再响应取消;旧网络回调按注册实例隔离。
|
||||
- 无线开关恢复分为“启用命令完成”和“网络实际可用”。测试先等关闭稳定,再启用并等待 Network 回调;一次重测通过不能单独证明此前恢复超时的根因。
|
||||
- 模拟器冷启动超时不代表 Compose 测试失败。先检查已有 Emulator、加速能力和登记系统镜像,不重复下载;可用独立进程记录启动诊断、确认 boot_completed 后运行选定场景。只关闭本次启动实例。
|
||||
|
||||
测试前阅读本文件及 [可重复测试流程](README.md)。这里维护可复用的方法;单次失败、重测次数、耗时与验收结果写入 `../测试结果归档/`。本页依据 [四栏版真机记录](../测试结果归档/2026-09-26_四栏界面优化.md) 整理,方法有效不等于所有同类错误均有相同根因。
|
||||
|
||||
## 1. root 命令在 instrumentation 中失败
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Android 按变更影响选测(当前规则)
|
||||
|
||||
WiFi 通道新增场景、前置条件和验收标准见 [WiFi 传输测试](WiFi传输测试.md)。`wifi-transport-recovery` 切手机无线开关,`wifi-transport-network` 重新应用设备当前网络,`wifi-transport-restart` 重启主服务,均不得并入普通全量或公共回归。`wifiTransport` 是独立模拟器场景,不接实机。
|
||||
|
||||
适用 MOBILE-TEST-SELECTION、TEST-MOBILE-SELECTION、TEST-MOBILE-INTEGRATION。本文优先于历史版本专项验收中的固定执行顺序。授权决定怎样操作设备,不代表每轮必须使用设备。
|
||||
|
||||
## 选择范围
|
||||
|
||||
Reference in New Issue
Block a user