mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-08 05:23:41 +08:00
a82935f326
Integrate onroad views, configurable widgets, local settings, steering-profile contracts, Bluetooth and Android Auto with their device and runtime services. Co-authored-by: Adeeb Shihadeh <adeebshihadeh@gmail.com> Co-authored-by: Daniel Koepping <elkoled@gmail.com> Co-authored-by: Danny <dannyirwin31@yahoo.com> Co-authored-by: DimitriChrysafis <Dimitri.chrysafis@gmail.com> Co-authored-by: Dom <168790843+firestar5683@users.noreply.github.com> Co-authored-by: Harald Schäfer <harald.the.engineer@gmail.com> Co-authored-by: Prabhaav Pillai <prabhaav@msu.edu> Co-authored-by: Shane Smiskol <shane@smiskol.com> Co-authored-by: dirwin31 <83434411+dirwin31@users.noreply.github.com> Co-authored-by: firestarsdog <229254897+firestarsdog@users.noreply.github.com> Co-authored-by: stef <19478336+stefpi@users.noreply.github.com> Co-authored-by: whoisdomi <dcantu83@gmail.com> Co-authored-by: Intelli <contact@intelli.software> Co-authored-by: pharmacomaniac <blittle65@gmail.com> Co-authored-by: prabhaavp <prabhaav@msu.edu>
364 lines
15 KiB
Python
364 lines
15 KiB
Python
import json
|
|
import inspect
|
|
import os
|
|
from pathlib import Path
|
|
import socket
|
|
import struct
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import threading
|
|
import time
|
|
import unittest
|
|
from unittest.mock import create_autospec, patch
|
|
|
|
from openpilot.starpilot.maps import operation_owner as ops
|
|
|
|
|
|
GEN_A = 'a' * 64
|
|
REGION = {'token': 'us_state.Oklahoma', 'name': 'Oklahoma', 'bounds': [33.6, -103.0, 37.0, -94.4],
|
|
'groups': 1, 'available': True, 'unavailable': ''}
|
|
|
|
|
|
def start_request(expected: str = '') -> dict:
|
|
return {'version': 1, 'op': 'start', 'regionToken': REGION['token'], 'maxTransferBytes': 1024,
|
|
'maxNewDiskBytes': 2048, 'expectedCurrentGeneration': expected}
|
|
|
|
|
|
class TestMapSnapshotOperationOwner(unittest.TestCase):
|
|
def setUp(self):
|
|
self.temp = tempfile.TemporaryDirectory()
|
|
self.addCleanup(self.temp.cleanup)
|
|
self.base = Path(self.temp.name).resolve()
|
|
self.root = self.base / 'starpilot/maps/offline'
|
|
self.current = ''
|
|
self.parked = True
|
|
|
|
def owner(self, execute, **kwargs):
|
|
owner = ops.MapSnapshotOperationOwner(root=self.root, marker=self.base / 'operation.json',
|
|
catalog=lambda: [REGION], parked=lambda: self.parked,
|
|
selected=lambda: self.current, execute=execute,
|
|
package_ready=lambda: True,
|
|
snapshot_info=lambda: {'generation': self.current, 'storedBytes': 32,
|
|
'tileCount': 1, 'bounds': REGION['bounds']}, **kwargs)
|
|
self.addCleanup(owner.close)
|
|
return owner
|
|
|
|
@staticmethod
|
|
def await_terminal(owner):
|
|
deadline = time.monotonic() + 3
|
|
while time.monotonic() < deadline:
|
|
status = owner.snapshot()
|
|
if status['state'] not in ops.ACTIVE:
|
|
return status
|
|
time.sleep(.01)
|
|
raise AssertionError('operation remained active')
|
|
|
|
def test_setup_reports_package_and_snapshot_independently_without_transfer(self):
|
|
executed = []
|
|
owner = self.owner(lambda *args: executed.append(args))
|
|
self.current = GEN_A
|
|
result = owner.dispatch({'version': 1, 'op': 'setup'})
|
|
self.assertTrue(result['packageReady'])
|
|
self.assertEqual(result['packageState'], 'ready')
|
|
self.assertTrue(result['snapshotReady'])
|
|
self.assertEqual(result['selectedGeneration'], GEN_A)
|
|
self.assertTrue(result['parked'])
|
|
self.assertGreater(result['freeDiskBytes'], 0)
|
|
self.assertEqual(executed, [])
|
|
self.assertFalse(self.root.exists())
|
|
self.parked = False
|
|
self.assertFalse(owner.setup()['parked'])
|
|
self.assertEqual(self.current, GEN_A)
|
|
|
|
def test_setup_invalid_package_does_not_clear_saved_selection(self):
|
|
owner = self.owner(lambda *args: self.fail('setup must not execute provider'))
|
|
owner._package_ready = lambda: False
|
|
self.current = GEN_A
|
|
with patch.object(ops.shadow, 'PROVIDER_BINARY', self.base / 'mapd'), patch.object(ops.shadow, 'PROVIDER_MANIFEST', self.base / 'manifest.json'):
|
|
self.assertEqual(owner.setup()['packageState'], 'missing_binary')
|
|
(self.base / 'mapd').write_bytes(b'bad')
|
|
self.assertEqual(owner.setup()['packageState'], 'missing_manifest')
|
|
(self.base / 'manifest.json').write_text('{}')
|
|
result = owner.setup()
|
|
self.assertEqual(result['packageState'], 'invalid_package')
|
|
self.assertTrue(result['snapshotReady'])
|
|
self.assertEqual(self.current, GEN_A)
|
|
with self.assertRaises(ops.OperationError):
|
|
owner.dispatch({'version': 1, 'op': 'setup', 'url': 'https://example.com'})
|
|
|
|
def test_first_install_prepare_then_select_and_status(self):
|
|
def execute(mode, args, progress):
|
|
self.assertTrue(self.root.is_dir())
|
|
if mode == '--snapshot-managed-prepare':
|
|
progress({'phase': 'transferring', 'completedGroups': 1, 'totalGroups': 1,
|
|
'transferredBytes': 512, 'transferBudgetBytes': 1024})
|
|
else:
|
|
self.assertIn('--expected-current', args)
|
|
self.current = GEN_A
|
|
return GEN_A
|
|
|
|
owner = self.owner(execute)
|
|
started = owner.start(start_request())
|
|
self.assertTrue(started['operationId'].startswith(owner._session + ':'))
|
|
done = self.await_terminal(owner)
|
|
self.assertEqual(done['state'], 'completed')
|
|
self.assertEqual(done['selectedGeneration'], GEN_A)
|
|
self.assertTrue(done['selectedForNextShadowStart'])
|
|
self.assertEqual(self.root.stat().st_mode & 0o777, 0o700)
|
|
|
|
def test_nonparked_or_changed_selection_never_creates_root(self):
|
|
for parked, current, code in ((False, '', 'not_parked'), (True, GEN_A, 'selection_changed')):
|
|
self.parked, self.current = parked, current
|
|
owner = self.owner(lambda *args: GEN_A)
|
|
with self.assertRaises(ops.OperationError) as caught:
|
|
owner.start(start_request())
|
|
self.assertEqual(caught.exception.code, code)
|
|
self.assertFalse(self.root.exists())
|
|
|
|
def test_prefixed_download_and_cancel_preserve_normal_selection(self):
|
|
normal = self.root
|
|
normal.mkdir(parents=True)
|
|
saved = b'{"version":1,"generation":"' + GEN_A.encode() + b'"}'
|
|
(normal / 'current.json').write_bytes(saved)
|
|
entered, release = threading.Event(), threading.Event()
|
|
def execute(mode, args, progress):
|
|
self.assertEqual(args[args.index('--offline-root') + 1], str(self.root))
|
|
entered.set()
|
|
self.assertTrue(release.wait(2))
|
|
return GEN_A
|
|
with patch.dict(os.environ, {'OPENPILOT_PREFIX': 'desk-check'}):
|
|
self.root = ops.offline_root(self.base)
|
|
with patch.object(ops, 'offline_root', return_value=self.root):
|
|
owner = ops.MapSnapshotOperationOwner(catalog=lambda: [REGION], parked=lambda: self.parked,
|
|
execute=execute, package_ready=lambda: True)
|
|
self.addCleanup(owner.close)
|
|
self.assertEqual(owner.root, self.base / 'starpilot-desk-check/maps/offline')
|
|
operation_id = owner.start(start_request())['operationId']
|
|
self.assertTrue(entered.wait(2))
|
|
owner.cancel(operation_id)
|
|
release.set()
|
|
self.assertEqual(self.await_terminal(owner)['state'], 'canceled')
|
|
self.assertEqual((normal / 'current.json').read_bytes(), saved)
|
|
self.assertFalse((self.root / 'current.json').exists())
|
|
|
|
def test_status_collects_evidence_but_start_rechecks_it(self):
|
|
owner = self.owner(lambda *args: GEN_A)
|
|
reads = []
|
|
def parked():
|
|
reads.append(self.parked)
|
|
return self.parked
|
|
owner._parked = parked
|
|
owner.catalog()
|
|
self.assertTrue(reads)
|
|
self.parked = False
|
|
with self.assertRaises(ops.OperationError) as caught:
|
|
owner.start(start_request())
|
|
self.assertEqual(caught.exception.code, 'not_parked')
|
|
self.assertEqual(reads[-1], False)
|
|
self.assertFalse(self.root.exists())
|
|
|
|
def test_symlink_ancestor_rejected_without_following_it(self):
|
|
external = self.base / 'external'
|
|
external.mkdir()
|
|
(self.base / 'starpilot').symlink_to(external, target_is_directory=True)
|
|
owner = self.owner(lambda *args: GEN_A)
|
|
with self.assertRaises(ops.OperationError):
|
|
owner.start(start_request())
|
|
self.assertFalse((external / 'maps').exists())
|
|
|
|
def test_cancel_and_selection_race_do_not_claim_completion(self):
|
|
begun = threading.Event()
|
|
release = threading.Event()
|
|
|
|
def execute(mode, args, progress):
|
|
if mode == '--snapshot-managed-prepare':
|
|
begun.set()
|
|
self.assertTrue(release.wait(2))
|
|
elif self.current != '':
|
|
raise ops.OperationError('selection_changed')
|
|
return GEN_A
|
|
|
|
owner = self.owner(execute)
|
|
op = owner.start(start_request())['operationId']
|
|
self.assertTrue(begun.wait(2))
|
|
owner.cancel(op)
|
|
release.set()
|
|
self.assertEqual(self.await_terminal(owner)['state'], 'canceled')
|
|
self.assertEqual(self.current, '')
|
|
|
|
self.current = ''
|
|
begun.clear()
|
|
release.clear()
|
|
owner.start(start_request())
|
|
self.assertTrue(begun.wait(2))
|
|
self.current = GEN_A
|
|
release.set()
|
|
self.assertEqual(self.await_terminal(owner)['errorCode'], 'selection_changed')
|
|
|
|
def test_cancel_after_acknowledged_select_reports_committed_selector(self):
|
|
holder = {}
|
|
|
|
def execute(mode, args, progress):
|
|
if mode == '--snapshot-select':
|
|
self.current = GEN_A
|
|
holder['owner']._cancel.set()
|
|
return GEN_A
|
|
|
|
owner = self.owner(execute)
|
|
holder['owner'] = owner
|
|
owner.start(start_request())
|
|
self.assertEqual(self.await_terminal(owner)['state'], 'completed')
|
|
self.assertTrue(owner.snapshot()['selectedForNextShadowStart'])
|
|
|
|
def test_absolute_receive_deadline_rejects_trickle(self):
|
|
sender, receiver = socket.socketpair()
|
|
self.addCleanup(sender.close)
|
|
self.addCleanup(receiver.close)
|
|
sender.sendall(struct.pack('!I', 4096) + b'{')
|
|
with self.assertRaises((ops.MapOperationUnavailable, TimeoutError)):
|
|
ops._receive(receiver, 4096, deadline_s=.05)
|
|
|
|
def test_client_receives_reply_after_one_second_within_total_deadline(self):
|
|
sender, receiver = socket.socketpair()
|
|
self.addCleanup(sender.close)
|
|
self.addCleanup(receiver.close)
|
|
|
|
def delayed_reply():
|
|
time.sleep(1.1)
|
|
sender.sendall(ops._encode({'version': 1, 'ok': True, 'result': {}}, ops.MAX_STATUS))
|
|
|
|
thread = threading.Thread(target=delayed_reply)
|
|
thread.start()
|
|
self.assertTrue(ops._receive(receiver, ops.MAX_STATUS, deadline_s=2)['ok'])
|
|
thread.join(2)
|
|
|
|
def test_catalog_accepts_bundled_oversize_unavailable_regions(self):
|
|
rows = [{**REGION, 'token': f'nation.region{i}'} for i in range(229)]
|
|
rows[0] = {**rows[0], 'groups': 120, 'available': False, 'unavailable': 'too_large'}
|
|
completed = subprocess.CompletedProcess([], 0, json.dumps(rows).encode(), b'')
|
|
with patch.object(ops.subprocess, 'run', return_value=completed):
|
|
self.assertEqual(ops._catalog()[0]['unavailable'], 'too_large')
|
|
|
|
def test_boolean_version_is_not_v1(self):
|
|
owner = self.owner(lambda *args: GEN_A)
|
|
with self.assertRaises(ops.OperationError) as caught:
|
|
owner.dispatch({'version': True, 'op': 'status'})
|
|
self.assertEqual(caught.exception.code, 'invalid_request')
|
|
|
|
def test_client_timeout_contract(self):
|
|
self.assertEqual(inspect.signature(ops.request_operation).parameters['timeout_s'].default, 8.0)
|
|
with self.assertRaisesRegex(ops.MapOperationUnavailable, 'invalid operation timeout'):
|
|
ops.request_operation({'version': 1, 'op': 'status'}, timeout_s=11)
|
|
|
|
def test_marker_io_does_not_block_cancellation(self):
|
|
writing = threading.Event()
|
|
release_write = threading.Event()
|
|
begun = threading.Event()
|
|
release_prepare = threading.Event()
|
|
|
|
def execute(mode, args, progress):
|
|
begun.set()
|
|
self.assertTrue(release_prepare.wait(2))
|
|
return GEN_A
|
|
|
|
owner = self.owner(execute)
|
|
|
|
def blocked_marker(value):
|
|
writing.set()
|
|
self.assertTrue(release_write.wait(2))
|
|
|
|
try:
|
|
with patch.object(owner, '_write_marker', side_effect=blocked_marker):
|
|
op = owner.start(start_request())['operationId']
|
|
self.assertTrue(writing.wait(1))
|
|
self.assertTrue(begun.wait(1))
|
|
before = time.monotonic()
|
|
owner.cancel(op)
|
|
self.assertLess(time.monotonic() - before, .2)
|
|
release_prepare.set()
|
|
self.assertEqual(self.await_terminal(owner)['state'], 'canceled')
|
|
finally:
|
|
release_write.set()
|
|
release_prepare.set()
|
|
|
|
@unittest.skipUnless(sys.platform == 'linux', 'Linux prctl contract')
|
|
def test_parent_death_guard_is_not_optimized_away(self):
|
|
command = ops._guarded_go_command(Path('/bin/true'), [])
|
|
command[1:1] = ['-O']
|
|
command[4] = '-1'
|
|
self.assertEqual(subprocess.run(command, check=False, timeout=2).returncode, 125)
|
|
|
|
@unittest.skipUnless(sys.platform == 'linux', 'Linux process and peer-credential contract')
|
|
def test_real_sigint_ignoring_child_is_killed_and_reaped(self):
|
|
script = self.base / 'ignore.py'
|
|
script.write_text('import signal,time\nsignal.signal(signal.SIGINT, signal.SIG_IGN)\nprint("bad-json", flush=True)\ntime.sleep(30)\n')
|
|
owner = self.owner(lambda *args: GEN_A)
|
|
with patch.object(ops.shadow, 'PROVIDER_BINARY', Path(sys.executable)):
|
|
with patch.object(ops, '_guarded_go_command', return_value=[sys.executable, str(script)]):
|
|
with self.assertRaises((ops.OperationError, ops.MapOperationUnavailable, ValueError)):
|
|
owner._run_go('--snapshot-managed-prepare', [], owner._progress)
|
|
self.assertIsNone(owner._child)
|
|
|
|
@unittest.skipUnless(sys.platform == 'linux', 'Linux peer-credential contract')
|
|
def test_disconnected_client_does_not_stop_server(self):
|
|
owner = self.owner(lambda *args: GEN_A)
|
|
stop = threading.Event()
|
|
path = self.base / 'operations.sock'
|
|
thread = threading.Thread(target=ops.serve, kwargs={'owner': owner, 'socket_path': path, 'stop': stop}, daemon=True)
|
|
thread.start()
|
|
deadline = time.monotonic() + 2
|
|
while not path.exists() and time.monotonic() < deadline:
|
|
time.sleep(.01)
|
|
with socket.socket(socket.AF_UNIX) as client:
|
|
client.connect(str(path))
|
|
client.sendall(struct.pack('!I', 30) + b'{')
|
|
nested = ('[' * 1500 + ']' * 1500).encode()
|
|
with socket.socket(socket.AF_UNIX) as client:
|
|
client.settimeout(3)
|
|
client.connect(str(path))
|
|
client.sendall(struct.pack('!I', len(nested)) + nested)
|
|
self.assertEqual(ops._receive(client, ops.MAX_STATUS)['error']['code'], 'invalid_request')
|
|
reply = ops.request_operation({'version': 1, 'op': 'status'}, socket_path=path, timeout_s=3)
|
|
self.assertTrue(reply['ok'])
|
|
stop.set()
|
|
thread.join(2)
|
|
self.assertFalse(thread.is_alive())
|
|
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|
|
|
|
|
|
class TestMapOperationEntrypoint(unittest.TestCase):
|
|
def test_public_parked_source_and_cleanup(self):
|
|
from openpilot.starpilot.galaxy.settings import LiveContextSource
|
|
evidence = create_autospec(LiveContextSource, instance=True, spec_set=True)
|
|
for failure in (None, RuntimeError('serve failed')):
|
|
with self.subTest(failure=failure), patch.object(ops.sys, 'platform', 'linux'), \
|
|
patch.object(ops, 'Params'), patch.object(ops, '_owned_socket_dir'), \
|
|
patch('openpilot.starpilot.galaxy.settings.LiveContextSource', return_value=evidence), \
|
|
patch.object(ops, 'MapSnapshotOperationOwner') as factory, \
|
|
patch.object(ops, 'serve', side_effect=failure) as serve:
|
|
evidence.reset_mock()
|
|
if failure is None:
|
|
ops.main()
|
|
else:
|
|
with self.assertRaisesRegex(RuntimeError, 'serve failed'):
|
|
ops.main()
|
|
evidence.parked.assert_called_once_with()
|
|
self.assertIs(factory.call_args.kwargs['parked'], evidence.parked)
|
|
serve.assert_called_once_with(factory.return_value)
|
|
factory.return_value.close.assert_called_once_with()
|
|
evidence.close.assert_called_once_with()
|
|
|
|
def test_evidence_closed_when_setup_fails(self):
|
|
from openpilot.starpilot.galaxy.settings import LiveContextSource
|
|
evidence = create_autospec(LiveContextSource, instance=True, spec_set=True)
|
|
with patch.object(ops.sys, 'platform', 'linux'), patch.object(ops, 'Params'), \
|
|
patch('openpilot.starpilot.galaxy.settings.LiveContextSource', return_value=evidence), \
|
|
patch.object(ops, '_owned_socket_dir', side_effect=OSError('socket directory')):
|
|
with self.assertRaisesRegex(OSError, 'socket directory'):
|
|
ops.main()
|
|
evidence.close.assert_called_once_with()
|