Improve controller behavior and update project requirements
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
"""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()
|
||||
Reference in New Issue
Block a user