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