Files

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()