Files
StarPilot/openpilot/starpilot/ui/tests/test_calibration_reset.py
T
firestar5683 a82935f326 User interfaces and Android Auto
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>
2026-10-14 20:02:39 -05:00

162 lines
7.1 KiB
Python

import fcntl
from pathlib import Path
from types import SimpleNamespace as NS
import tempfile
import time
import unittest
from unittest.mock import patch
import pyray as rl
from openpilot.common.params import Params
from openpilot.selfdrive.ui.layouts.settings import device
from openpilot.selfdrive.ui.mici.layouts.settings.device import device_layout
from openpilot.starpilot.ui.calibration_reset import CalibrationReset, RESET_KEY
from openpilot.system.ui.lib.application import gui_app
from openpilot.system.ui.widgets import DialogResult
class TestCalibrationReset(unittest.TestCase):
def setUp(self):
directory = tempfile.TemporaryDirectory()
self.addCleanup(directory.cleanup)
self.root = Path(directory.name)
self.params = Params(directory.name)
self.reset = CalibrationReset()
self.ui = NS(params=self.params, engaged=False, ignition=False, is_offroad=lambda: True,
prime_state=NS(is_paired=lambda: False))
self.enterContext(patch.object(device_layout, "ui_state", self.ui))
self.enterContext(patch.object(device_layout, "calibration_reset", self.reset))
self.enterContext(patch.object(device, "ui_state", self.ui))
self.enterContext(patch.object(device, "calibration_reset", self.reset))
self.enterContext(patch.object(gui_app, "font", return_value=rl.Font()))
self.enterContext(patch.object(gui_app, "texture", return_value=NS(width=64, height=64)))
self.pushed = self.enterContext(patch.object(gui_app, "push_widget"))
with patch.object(device_layout, "Params", return_value=self.params):
self.panel = device_layout.DeviceLayoutMici()
self.button = next(item for item in self.panel._scroller._items
if isinstance(item, device_layout.EngagedConfirmationButton) and item.get_text() == "reset calibration")
def wait_for_reset(self):
deadline = time.monotonic() + 3
while self.reset.pending and time.monotonic() < deadline:
time.sleep(.01)
self.assertFalse(self.reset.pending, "Calibration reset did not finish")
def confirm(self):
assert self.button._click_callback is not None
self.button._click_callback()
self.pushed.call_args.args[0]._confirm_callback()
def test_native_confirmation_returns_during_params_contention_and_coalesces(self):
keys = ("CalibrationParams", "LiveTorqueParameters", "LiveParametersV2", "LiveDelay")
for key in keys:
self.params.put(key, b"existing learned cache", block=True)
self.params.put_bool("ShowSpeedLimits", False, block=True)
with (self.root / ".lock").open("a") as lock:
fcntl.flock(lock, fcntl.LOCK_EX)
try:
start = time.monotonic()
self.confirm()
self.assertLess(time.monotonic() - start, .5)
self.assertTrue(self.reset.pending)
self.assertFalse(self.button.enabled)
self.assertFalse(self.reset.request(self.params))
self.assertFalse(self.params.get_bool("OnroadCycleRequested"))
self.assertIsNotNone(self.params.get("CalibrationParams"))
finally:
fcntl.flock(lock, fcntl.LOCK_UN)
self.wait_for_reset()
for key in keys:
self.assertFalse(Path(self.params.get_param_path(key)).exists())
self.assertTrue(self.params.get_bool("OnroadCycleRequested"))
self.assertFalse(self.params.get_bool("DoReboot"))
self.assertFalse(self.params.get_bool("ShowSpeedLimits"))
self.assertFalse(self.reset.request(self.params))
def test_reset_generation_precedes_deletion_and_preserves_learning_choice(self):
preference = b'{"version":1,"vehicles":{"hyundai":{"HYUNDAI_IONIQ_6":true}}}'
self.params.put("SteeringLearningPreferences", preference, block=True)
self.params.put(RESET_KEY, "old-generation", block=True)
self.params.put_bool("ForceAutoTuneOff", True, block=True)
self.params.put("SteerLatAccel", 3.3, block=True)
removed = []
remove = self.params.remove
def recorded_remove(key):
generation = self.params.get(RESET_KEY)
assert isinstance(generation, str)
self.assertEqual(len(generation), 32)
self.assertTrue(all(char in "0123456789abcdef" for char in generation))
removed.append((key, generation))
remove(key)
with patch.object(self.params, "remove", side_effect=recorded_remove):
self.reset._run(self.params)
self.assertIsNone(self.reset.take_error())
self.assertEqual(len(removed), 4)
self.assertEqual(len({generation for _, generation in removed}), 1)
self.assertEqual(self.params.get("SteeringLearningPreferences"), preference)
self.assertTrue(self.params.get_bool("ForceAutoTuneOff"))
self.assertEqual(self.params.get("SteerLatAccel"), 3.3)
self.assertTrue(self.params.get_bool("OnroadCycleRequested"))
def test_failed_reset_generation_preserves_caches_and_does_not_request_cycle(self):
self.params.put("CalibrationParams", b"saved", block=True)
with patch.object(self.params, "put", side_effect=RuntimeError("write failed")), \
patch.object(self.params, "remove") as remove:
self.reset._run(self.params)
remove.assert_not_called()
self.assertEqual(self.params.get("CalibrationParams"), b"saved")
self.assertFalse(self.params.get_bool("OnroadCycleRequested"))
self.assertIsNotNone(self.reset.take_error())
def test_confirmation_rechecks_engagement(self):
self.params.put("CalibrationParams", b"saved", block=True)
assert self.button._click_callback is not None
self.button._click_callback()
confirm = self.pushed.call_args.args[0]._confirm_callback
self.ui.engaged = True
confirm()
self.assertFalse(self.button.enabled)
self.assertEqual(self.params.get("CalibrationParams"), b"saved")
self.assertFalse(self.params.get_bool("OnroadCycleRequested"))
def test_c3_cancel_and_action_guard_prevent_reset(self):
panel = device.DeviceLayout.__new__(device.DeviceLayout)
panel._params = self.params
panel._calibration_reset_pending = False
guard = [True]
with patch.object(device, "ConfirmDialog") as dialog:
panel._reset_calibration_prompt(lambda: guard[0])
confirm = dialog.call_args.kwargs["callback"]
confirm(DialogResult.CANCEL)
guard[0] = False
confirm(DialogResult.CONFIRM)
self.assertFalse(self.reset.pending)
self.assertFalse(self.params.get_bool("OnroadCycleRequested"))
guard[0] = True
confirm(DialogResult.CONFIRM)
self.wait_for_reset()
self.assertTrue(self.params.get_bool("OnroadCycleRequested"))
def test_absent_caches_reset_normally(self):
self.confirm()
self.wait_for_reset()
self.assertIsNone(self.reset.take_error())
self.assertTrue(self.params.get_bool("OnroadCycleRequested"))
def test_failed_delete_is_reported_once_without_requesting_cycle(self):
self.params.put("CalibrationParams", b"saved", block=True)
with patch.object(self.params, "remove", return_value=None):
self.confirm()
self.wait_for_reset()
self.assertFalse(self.params.get_bool("OnroadCycleRequested"))
self.assertEqual(self.params.get("CalibrationParams"), b"saved")
self.pushed.reset_mock()
self.panel._update_state()
self.pushed.assert_called_once()
self.panel._update_state()
self.pushed.assert_called_once()
self.assertFalse(self.reset.pending)