121 lines
7.8 KiB
Python
121 lines
7.8 KiB
Python
"""Host-only safety tests: selection must never imply device or network access."""
|
|
import contextlib
|
|
import io
|
|
import json
|
|
from pathlib import Path
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import types
|
|
import unittest
|
|
from unittest.mock import patch
|
|
import run_ble_test as device
|
|
import run_emulator_ui_test as emulator
|
|
|
|
class SelectionTests(unittest.TestCase):
|
|
def quiet(self): return contextlib.redirect_stdout(io.StringIO())
|
|
def test_previews_need_no_device_or_dependencies(self):
|
|
with patch('subprocess.run', side_effect=AssertionError('unexpected device access')), self.quiet():
|
|
for runner, name in ((device, 'navigation'), (emulator, 'settings')):
|
|
self.assertEqual(runner.main(['--list-scenarios']), 0)
|
|
self.assertEqual(runner.main(['--scenario', name, '--dry-run']), 0)
|
|
self.assertEqual(runner.main([]), 2)
|
|
def test_one_scenario_cannot_expand_to_ble_or_network(self):
|
|
calls = []
|
|
fake = types.SimpleNamespace(main=lambda args: calls.append(args))
|
|
with patch.dict(sys.modules, {'device_test_runner': fake}), self.quiet():
|
|
self.assertEqual(device.main(['--scenario', 'navigation', '--adb', 'fake', '--build', 'fake']), 0)
|
|
self.assertEqual(len(calls), 1)
|
|
self.assertEqual(calls[0][calls[0].index('--scenario') + 1], 'navigation')
|
|
self.assertNotIn('--reapply-current-wifi', calls[0])
|
|
self.assertEqual(calls[0][calls[0].index('--poll-cycles') + 1], '0')
|
|
def test_failure_stops_later_scenarios(self):
|
|
calls = []
|
|
def fail(args):
|
|
calls.append(args); raise RuntimeError('expected')
|
|
with patch.dict(sys.modules, {'device_test_runner': types.SimpleNamespace(main=fail)}), self.quiet():
|
|
with self.assertRaises(RuntimeError):
|
|
device.main(['--scenario', 'connection', '--scenario', 'data', '--adb', 'fake', '--build', 'fake'])
|
|
self.assertEqual(len(calls), 1)
|
|
def test_common_does_not_include_disruptive_scenarios(self):
|
|
self.assertFalse(set(device.COMMON) & {'wifi-reapply', 'controls', 'polling', 'bluetooth-toggle', 'rename', 'remember'})
|
|
def test_zero_tests_or_crash_is_not_success(self):
|
|
self.assertTrue(emulator.passed('OK (1 test)\n'))
|
|
for text in ('OK (0 tests)', 'OK (2 tests)', 'FAILURES!!!\nOK (1 test)', 'Process crashed\nOK (1 test)'):
|
|
self.assertFalse(emulator.passed(text))
|
|
def test_failed_instrumentation_restores_each_setting_and_records_failure(self):
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
b = Path(tmp); apk = b / 'uiTest/build/outputs/apk/debug/uiTest-debug.apk'
|
|
apk.parent.mkdir(parents=True); apk.write_bytes(b'fixture')
|
|
state = {'font_scale': '1.0', 'accelerometer_rotation': '1', 'user_rotation': '0'}
|
|
history = []
|
|
def run(cmd, **kwargs):
|
|
history.append(cmd)
|
|
args = cmd[3:]
|
|
out = ''
|
|
if args == ['get-state']: out = 'device'
|
|
elif args[:2] == ['shell', 'getprop']: out = '1'
|
|
elif args == ['emu', 'avd', 'name']: out = 'fixture\nOK'
|
|
elif args[:3] == ['shell', 'settings', 'get']: out = state[args[4]]
|
|
elif args[:3] == ['shell', 'settings', 'put']: state[args[4]] = args[5]
|
|
elif args == ['shell', 'wm', 'size']: out = 'Physical size: 1080x1920'
|
|
elif args == ['shell', 'wm', 'density']: out = 'Physical density: 420'
|
|
elif args[:1] == ['install']: out = 'Success'
|
|
elif args[:3] == ['shell', 'am', 'instrument']: raise subprocess.TimeoutExpired(cmd, 120)
|
|
return subprocess.CompletedProcess(cmd, 0, out, '')
|
|
report = b / 'report.json'
|
|
with patch('subprocess.run', side_effect=run), self.quiet():
|
|
result = emulator.main(['--scenario', 'navigation', '--avd', 'fixture', '--adb', 'fake', '--build', str(b), '--report', str(report)])
|
|
self.assertEqual(result, 1)
|
|
self.assertEqual(state, {'font_scale': '1.0', 'accelerometer_rotation': '1', 'user_rotation': '0'})
|
|
evidence = json.loads(report.read_text(encoding='utf-8'))
|
|
self.assertEqual(evidence['tests'][0]['result'], 'failed')
|
|
self.assertIn('timed out', evidence['failure'])
|
|
self.assertFalse(evidence['cleanup_errors'])
|
|
self.assertTrue(any(cmd[3:6] == ['shell', 'am', 'force-stop'] for cmd in history))
|
|
def test_wifi_modes_are_explicit_and_dry_run_has_no_remote_import(self):
|
|
import run_wifi_switch_test as wifi
|
|
for argv in (['run_wifi_switch_test.py', '--list-scenarios'],
|
|
['run_wifi_switch_test.py', '--validate-only', '--dry-run']):
|
|
with patch.object(sys, 'argv', argv), patch('subprocess.run', side_effect=AssertionError('unexpected access')), patch.dict(sys.modules, {'remote_support': None}), self.quiet():
|
|
self.assertEqual(wifi.main(), 0)
|
|
for argv in (['run_wifi_switch_test.py'], ['run_wifi_switch_test.py', '--wrong-only', '--skip-wrong', '--dry-run']):
|
|
with patch.object(sys, 'argv', argv), self.quiet(), contextlib.redirect_stderr(io.StringIO()):
|
|
with self.assertRaises(SystemExit): wifi.main()
|
|
|
|
def test_real_navigation_adapter_has_no_board_access_and_restores_permissions(self):
|
|
import device_test_runner as runner
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
base = Path(tmp)
|
|
for name in ('debug/androidApp-debug.apk', 'androidTest/debug/androidApp-debug-androidTest.apk'):
|
|
apk = base / 'androidApp/build/outputs/apk' / name
|
|
apk.parent.mkdir(parents=True, exist_ok=True); apk.write_bytes(b'fixture')
|
|
grants = {'BLUETOOTH_SCAN': True, 'BLUETOOTH_CONNECT': False}
|
|
calls = []
|
|
def execute(command, **kwargs):
|
|
calls.append(command)
|
|
args = command[3:]; out = ''
|
|
if args == ['shell', 'su', '-c', 'id -u']: out = '0'
|
|
elif args == ['shell', 'getprop', 'ro.build.version.sdk']: out = '35'
|
|
elif args == ['shell', 'dumpsys', 'package', 'org.qimiaoscreen.controller']:
|
|
out = '\n'.join('android.permission.' + k + ': granted=' + str(v).lower() for k, v in grants.items())
|
|
elif args[:3] == ['shell', 'su', '-c'] and args[3].startswith('pm '):
|
|
operation = args[3].split(); grants[operation[-1].split('.')[-1]] = operation[1] == 'grant'
|
|
elif args[:3] == ['shell', 'am', 'instrument']:
|
|
self.assertFalse(any(grants.values()))
|
|
self.assertNotIn('device_name', args)
|
|
self.assertNotIn('library_order_hash', args)
|
|
out = 'OK (1 test)\n'
|
|
elif args[:1] == ['install']: out = 'Success'
|
|
return subprocess.CompletedProcess(command, 0, out, '')
|
|
with patch.object(runner, 'load_registration', return_value=[{'alias': 'android-test-a', 'serial': 'fixture-serial'}]), patch.object(runner, 'check_connected'), patch('subprocess.run', side_effect=execute), patch.dict(sys.modules, {'remote_support': None}), self.quiet():
|
|
runner.main(['--scenario', 'navigation', '--test', 'activity', '--poll-cycles', '0', '--adb', 'fake', '--build', str(base)])
|
|
self.assertEqual(grants, {'BLUETOOTH_SCAN': True, 'BLUETOOTH_CONNECT': False})
|
|
self.assertTrue(any(cmd[3:6] == ['shell', 'am', 'force-stop'] for cmd in calls))
|
|
|
|
def test_invalid_scenario_rejected_before_any_io(self):
|
|
with patch('subprocess.run', side_effect=AssertionError('unexpected access')), self.quiet(), contextlib.redirect_stderr(io.StringIO()):
|
|
with self.assertRaises(SystemExit): device.main(['--scenario', 'everything'])
|
|
|
|
if __name__ == '__main__': unittest.main()
|