mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-10-28 09:03:46 +08:00
180 lines
8.4 KiB
Python
180 lines
8.4 KiB
Python
import ast
|
|
import copy
|
|
import hashlib
|
|
import importlib.util
|
|
import json
|
|
import math
|
|
import os
|
|
from pathlib import Path
|
|
import sys
|
|
import tempfile
|
|
from types import SimpleNamespace
|
|
import unittest
|
|
from unittest.mock import patch
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
|
|
|
|
def load(name, path):
|
|
spec = importlib.util.spec_from_file_location(name, path)
|
|
assert spec is not None
|
|
module = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(module)
|
|
return module
|
|
|
|
|
|
migration = load('external_reset_state_helpers', ROOT / 'state_migration.py')
|
|
owner_ast = ast.parse((ROOT / 'navigation' / 'owner.py').read_text())
|
|
selected: list[ast.stmt] = [node for node in owner_ast.body if isinstance(node, (ast.FunctionDef, ast.ClassDef))
|
|
and node.name in ('ValidationError', 'destination', 'favorite_place')]
|
|
selected.extend(node for node in owner_ast.body if isinstance(node, ast.Assign)
|
|
and any(isinstance(target, ast.Name) and target.id == 'ROUTE_AVOIDANCES' for target in node.targets))
|
|
owner_class = next(node for node in owner_ast.body if isinstance(node, ast.ClassDef) and node.name == 'NavigationOwner')
|
|
selected.append(next(node for node in owner_class.body if isinstance(node, ast.FunctionDef) and node.name == 'read'))
|
|
namespace: dict = {'json': json, 'math': math, 'hashlib': hashlib, 'os': os, 'copy': copy, 'MAX_DOCUMENT': 256 * 1024,
|
|
'MAX_FAVORITES': 100, 'MAX_RECENTS': 10, 'FAVORITE_LABELS': ('home', 'work'),
|
|
'GPS_MODES': next(ast.literal_eval(node.value) for node in ast.parse((ROOT / 'gps' / 'source.py').read_text()).body
|
|
if isinstance(node, ast.Assign) and any(getattr(t, 'id', '') == 'GPS_MODES' for t in node.targets))}
|
|
exec(compile(ast.Module(body=selected, type_ignores=[]), str(ROOT / 'navigation' / 'owner.py'), 'exec'), namespace)
|
|
NavigationOwner = type('NavigationOwner', (), {'read': namespace['read']})
|
|
with patch.dict(sys.modules, {'openpilot.starpilot.state_migration': migration}):
|
|
reset = load('external_reset_subject', ROOT / 'external_preferences_reset.py')
|
|
|
|
|
|
class TestExternalPreferencesReset(unittest.TestCase):
|
|
def setUp(self):
|
|
temporary = tempfile.TemporaryDirectory()
|
|
self.addCleanup(temporary.cleanup)
|
|
self.root = Path(temporary.name)
|
|
self.storage = self.root / 'storage'
|
|
self.models = self.root / 'models' / 'v26'
|
|
self.models.mkdir(parents=True)
|
|
self.navigation = self.storage / 'navigation'
|
|
self.navigation.mkdir(parents=True)
|
|
self.model_file = self.models / 'preferences.json'
|
|
self.nav_file = self.navigation / 'settings.json'
|
|
self.original: dict = {'model_preferences': b'{"small":"old","randomizer":true}',
|
|
'navigation_settings': json.dumps({'version': 1, 'revision': 'old', 'enabled': True,
|
|
'token': 'credential', 'destination': {'name': 'Home', 'latitude': 1, 'longitude': 2},
|
|
'favorites': [], 'routeChoice': 2}).encode()}
|
|
self.model_file.write_bytes(self.original['model_preferences'])
|
|
self.nav_file.write_bytes(self.original['navigation_settings'])
|
|
self.payload = self.models / 'model.thneed'
|
|
self.payload.write_bytes(b'model-payload')
|
|
patcher = patch.dict(sys.modules, {'openpilot.starpilot.navigation.owner': SimpleNamespace(NavigationOwner=NavigationOwner)})
|
|
patcher.start()
|
|
self.addCleanup(patcher.stop)
|
|
|
|
def run_reset(self):
|
|
return reset.reset_external_preferences(self.storage, self.models)
|
|
|
|
def archive(self):
|
|
journal = json.loads((self.storage / 'fresh-external-profile-v2' / 'journal.json').read_bytes())
|
|
return migration.load_snapshot(Path(journal['snapshot']))
|
|
|
|
def test_fresh_defaults_archive_and_credentials(self):
|
|
self.assertEqual(self.run_reset(), ())
|
|
self.assertFalse(self.model_file.exists())
|
|
nav = json.loads(self.nav_file.read_bytes())
|
|
self.assertEqual(nav['token'], 'credential')
|
|
self.assertFalse(nav['enabled'])
|
|
self.assertIsNone(nav['destination'])
|
|
self.assertEqual(nav['favorites'], [])
|
|
self.assertEqual(nav['routeChoice'], 0)
|
|
self.assertNotEqual(nav['revision'], 'old')
|
|
self.assertEqual(self.archive(), self.original)
|
|
self.assertEqual(self.payload.read_bytes(), b'model-payload')
|
|
|
|
def test_restart_preserves_new_choices(self):
|
|
self.run_reset()
|
|
self.model_file.write_bytes(b'new model choice')
|
|
self.nav_file.write_bytes(b'new navigation choice')
|
|
self.run_reset()
|
|
self.assertEqual(self.model_file.read_bytes(), b'new model choice')
|
|
self.assertEqual(self.nav_file.read_bytes(), b'new navigation choice')
|
|
|
|
def test_fresh_profile_keeps_existing_galaxy_pairing_bytes(self):
|
|
galaxy = self.storage / 'galaxy'
|
|
galaxy.mkdir(mode=0o700)
|
|
values: dict = {'remote-v1.json': b'{"version":1,"slug":"ExistingGalaxy01","authHash":"' + b'a' * 64 +
|
|
b'","session":"' + b'b' * 64 + b'"}', 'access-v1.json': b'existing password verifier',
|
|
'remote-unpaired-v1': b''}
|
|
for name, raw in values.items():
|
|
(galaxy / name).write_bytes(raw)
|
|
(galaxy / name).chmod(0o600)
|
|
self.run_reset()
|
|
self.assertEqual({path.name: path.read_bytes() for path in galaxy.iterdir()}, values)
|
|
self.assertFalse(self.model_file.exists())
|
|
|
|
def test_invalid_navigation_archived_and_disabled(self):
|
|
for raw in (b'broken JSON', b'{"token":"credential"}', self.original['navigation_settings'].replace(b'"token": "credential"', b'"token": 4')):
|
|
with self.subTest(raw=raw):
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
storage = Path(directory) / 'storage'
|
|
(storage / 'navigation').mkdir(parents=True)
|
|
path = storage / 'navigation' / 'settings.json'
|
|
path.write_bytes(raw)
|
|
self.assertTrue(reset.reset_external_preferences(storage, Path(directory) / 'models'))
|
|
fresh = json.loads(path.read_bytes())
|
|
self.assertEqual(fresh['token'], '')
|
|
self.assertFalse(fresh['enabled'])
|
|
journal = json.loads((storage / 'fresh-external-profile-v2' / 'journal.json').read_bytes())
|
|
self.assertEqual(migration.load_snapshot(Path(journal['snapshot']))['navigation_settings'], raw)
|
|
|
|
def test_archive_failure_changes_nothing(self):
|
|
with patch.object(reset, '_save_snapshot', side_effect=OSError('disk full')):
|
|
with self.assertRaises(OSError):
|
|
self.run_reset()
|
|
self.assertEqual(self.model_file.read_bytes(), self.original['model_preferences'])
|
|
self.assertEqual(self.nav_file.read_bytes(), self.original['navigation_settings'])
|
|
|
|
def test_partial_write_resumes_without_repeated_model_reset(self):
|
|
atomic = reset._atomic_write
|
|
def fail_navigation(path, data):
|
|
if path == self.nav_file:
|
|
raise OSError('interrupted')
|
|
return atomic(path, data)
|
|
with patch.object(reset, '_atomic_write', side_effect=fail_navigation):
|
|
with self.assertRaises(OSError):
|
|
self.run_reset()
|
|
self.assertFalse(self.model_file.exists())
|
|
self.assertFalse((self.storage / 'fresh-external-profile-v2' / 'complete.json').exists())
|
|
self.run_reset()
|
|
self.assertFalse(json.loads(self.nav_file.read_bytes())['enabled'])
|
|
self.assertEqual(self.archive(), self.original)
|
|
|
|
def test_partial_reset_refuses_overwriting_new_choice(self):
|
|
atomic = reset._atomic_write
|
|
def fail_navigation(path, data):
|
|
if path == self.nav_file:
|
|
raise OSError('interrupted')
|
|
return atomic(path, data)
|
|
with patch.object(reset, '_atomic_write', side_effect=fail_navigation):
|
|
with self.assertRaises(OSError):
|
|
self.run_reset()
|
|
self.model_file.write_bytes(b'new choice')
|
|
with self.assertRaisesRegex(ValueError, 'changed during reset'):
|
|
self.run_reset()
|
|
self.assertEqual(self.model_file.read_bytes(), b'new choice')
|
|
self.assertEqual(self.nav_file.read_bytes(), self.original['navigation_settings'])
|
|
|
|
def test_absent_preferences_stay_absent(self):
|
|
self.model_file.unlink()
|
|
self.nav_file.unlink()
|
|
self.run_reset()
|
|
self.assertFalse(self.model_file.exists())
|
|
self.assertFalse(self.nav_file.exists())
|
|
self.assertEqual(self.archive(), {})
|
|
|
|
def test_symlink_preferences_rejected_before_mutation(self):
|
|
self.model_file.unlink()
|
|
self.model_file.symlink_to(self.payload)
|
|
with self.assertRaises(OSError):
|
|
self.run_reset()
|
|
self.assertEqual(self.payload.read_bytes(), b'model-payload')
|
|
self.assertEqual(self.nav_file.read_bytes(), self.original['navigation_settings'])
|
|
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|