Files
StarPilot/selfdrive/ui/tests/test_ui_param_cache.py
T

326 lines
9.4 KiB
Python

import threading
import pytest
from openpilot.selfdrive.ui.lib import ui_param_cache
from openpilot.selfdrive.ui.lib.ui_param_cache import UIParamCache
class FakeParams:
def __init__(self):
self.values = {"enabled": False, "count": 1}
self.calls = []
def get(self, key, **_kwargs):
self.calls.append(("get", key))
return self.values.get(key)
def get_bool(self, key, **_kwargs):
self.calls.append(("get_bool", key))
return bool(self.values.get(key, False))
def get_int(self, key, **_kwargs):
self.calls.append(("get_int", key))
return int(self.values.get(key, 0))
def get_float(self, key, **_kwargs):
self.calls.append(("get_float", key))
return float(self.values.get(key, 0.0))
def put(self, key, value, **_kwargs):
self.values[key] = value
def put_bool(self, key, value, **_kwargs):
self.values[key] = value
def put_int(self, key, value, **_kwargs):
self.values[key] = value
def put_float(self, key, value, **_kwargs):
self.values[key] = value
def put_nonblocking(self, key, value):
self.values[key] = value
def put_bool_nonblocking(self, key, value):
self.values[key] = value
def remove(self, key):
self.values.pop(key, None)
def clear_all(self, marker=None):
self.calls.append(("clear_all", marker))
self.values.clear()
def test_reads_are_shared_until_ttl():
now = [0.0]
params = FakeParams()
cached = UIParamCache(params, ttl=0.1, clock=lambda: now[0])
assert not cached.get_bool("enabled")
assert not cached.get_bool("enabled")
assert params.calls == [("get_bool", "enabled")]
now[0] = 0.11
params.values["enabled"] = True
assert cached.get_bool("enabled")
assert params.calls.count(("get_bool", "enabled")) == 2
def test_writes_invalidate_immediately():
params = FakeParams()
cached = UIParamCache(params, ttl=10.0)
assert not cached.get_bool("enabled")
cached.put_bool("enabled", True)
assert cached.get_bool("enabled")
assert params.calls.count(("get_bool", "enabled")) == 2
@pytest.mark.parametrize("background", [False, True])
def test_zero_ttl_disables_caching(background):
params = FakeParams()
cached = UIParamCache(params, ttl=0.0)
if background:
cached.start()
try:
assert not cached.get_bool("enabled")
params.values["enabled"] = True
assert cached.get_bool("enabled")
assert params.calls.count(("get_bool", "enabled")) == 2
finally:
cached.stop()
@pytest.mark.parametrize("method", ["put", "put_bool", "put_int", "put_float", "put_nonblocking", "put_bool_nonblocking"])
def test_all_write_paths_invalidate(method):
params = FakeParams()
cached = UIParamCache(params, ttl=10.0)
assert not cached.get_bool("enabled")
getattr(cached, method)("enabled", True)
assert cached.get_bool("enabled")
assert params.calls.count(("get_bool", "enabled")) == 2
def test_remove_and_clear_all_invalidate():
params = FakeParams()
cached = UIParamCache(params, ttl=10.0)
assert cached.get_int("count") == 1
cached.remove("count")
assert cached.get_int("count") == 0
assert not cached.get_bool("enabled")
params.values["enabled"] = True
cached.clear_all("flag")
params.values["enabled"] = True
assert cached.get_bool("enabled")
assert ("clear_all", "flag") in params.calls
class ControlledParams(FakeParams):
def __init__(self):
super().__init__()
self.block_next = False
self.fail_next = False
self.read_started = threading.Event()
self.release_read = threading.Event()
self.reader_threads = []
def get_int(self, key, *args, **kwargs):
self.reader_threads.append(threading.current_thread())
value = super().get_int(key, **kwargs)
if self.block_next:
self.block_next = False
self.read_started.set()
if not self.release_read.wait(3):
raise TimeoutError("Blocked parameter test read was not released")
if self.fail_next:
self.fail_next = False
raise OSError("Parameter temporarily unavailable")
return value
@pytest.fixture
def async_cache():
params = ControlledParams()
now = [0.0]
cache = UIParamCache(params, ttl=0.1, clock=lambda: now[0])
cache.start()
try:
yield params, cache, now
finally:
params.release_read.set()
cache.stop()
def test_background_cache_keeps_cold_reads_synchronous(async_cache):
params, cache, _ = async_cache
assert cache.get_int("count") == 1
assert params.reader_threads == [threading.current_thread()]
def test_stale_read_returns_while_refresh_is_blocked_and_deduplicates(async_cache):
params, cache, now = async_cache
assert cache.get_int("count") == 1
params.values["count"] = 2
params.block_next = True
now[0] = 0.2
assert cache.get_int("count") == 1
assert params.read_started.wait(1)
for _ in range(100):
assert cache.get_int("count") == 1
assert len(params.calls) == 2
assert len(cache._pending) == 1
now[0] = 0.5
params.release_read.set()
cache._refresh_queue.join()
assert cache.get_int("count") == 2
assert len(params.calls) == 2
assert not cache._pending
@pytest.mark.parametrize("clear_all", [False, True])
def test_invalidating_an_inflight_refresh_preserves_new_reads(async_cache, clear_all):
params, cache, now = async_cache
assert cache.get_int("count") == 1
params.block_next = True
now[0] = 0.2
assert cache.get_int("count") == 1
assert params.read_started.wait(1)
if clear_all:
cache.clear_all()
params.values["count"] = 3
else:
cache.put_int("count", 3)
assert cache.get_int("count") == 3
params.release_read.set()
cache._refresh_queue.join()
assert cache.get_int("count") == 3
def test_background_errors_keep_value_and_allow_later_refresh(async_cache):
params, cache, now = async_cache
assert cache.get_int("count") == 1
params.fail_next = True
params.values["count"] = 2
now[0] = 0.2
assert cache.get_int("count") == 1
cache._refresh_queue.join()
assert cache.get_int("count") == 1
assert cache._worker.is_alive()
assert len(params.calls) == 2
now[0] = 0.4
assert cache.get_int("count") == 1
cache._refresh_queue.join()
assert cache.get_int("count") == 2
def test_blocking_reads_remain_synchronous(async_cache):
params, cache, now = async_cache
assert cache.get_int("count", block=True) == 1
params.values["count"] = 2
now[0] = 0.2
assert cache.get_int("count", block=True) == 2
assert params.reader_threads == [threading.current_thread()] * 2
assert not cache._pending
def test_background_cache_does_not_refresh_unused_settings(async_cache):
params, cache, now = async_cache
assert cache.get_bool("enabled") is False
assert cache.get_int("count") == 1
now[0] = 0.2
cache.get_int("count")
cache._refresh_queue.join()
assert params.calls.count(("get_bool", "enabled")) == 1
assert params.calls.count(("get_int", "count")) == 2
def test_stop_releases_worker_and_restores_synchronous_refresh(async_cache):
params, cache, now = async_cache
assert cache.get_int("count") == 1
worker = cache._worker
cache.start()
assert cache._worker is worker
cache.stop()
assert not worker.is_alive()
params.values["count"] = 2
now[0] = 0.2
assert cache.get_int("count") == 2
assert params.reader_threads == [threading.current_thread()] * 2
def test_timed_out_worker_cannot_publish_after_restart(async_cache):
params, cache, now = async_cache
assert cache.get_int("count") == 1
params.values["count"] = 2
params.block_next = True
now[0] = 0.2
assert cache.get_int("count") == 1
assert params.read_started.wait(1)
old_worker, old_queue = cache._worker, cache._refresh_queue
cache.stop(timeout=0.01)
assert old_worker.is_alive()
params.values["count"] = 3
cache.start()
assert cache.get_int("count") == 1
cache._refresh_queue.join()
assert cache.get_int("count") == 3
params.release_read.set()
old_queue.join()
old_worker.join(1)
assert not old_worker.is_alive()
assert cache.get_int("count") == 3
def test_worker_lowers_its_own_realtime_priority(monkeypatch):
calls = []
configured = threading.Event()
def set_scheduler(pid, policy, priority):
calls.append((pid, policy, priority, threading.current_thread()))
configured.set()
monkeypatch.setattr(ui_param_cache, "PC", False)
monkeypatch.setattr(ui_param_cache.os, "SCHED_OTHER", 0, raising=False)
monkeypatch.setattr(ui_param_cache.os, "sched_param", lambda priority: priority, raising=False)
monkeypatch.setattr(ui_param_cache.os, "sched_setscheduler", set_scheduler, raising=False)
cache = UIParamCache(FakeParams())
cache.start()
try:
assert configured.wait(1)
assert calls == [(0, 0, 0, cache._worker)]
finally:
cache.stop()
def test_worker_priority_failure_restores_synchronous_reads(monkeypatch):
def set_scheduler(*_):
raise PermissionError("Unable to change scheduler")
monkeypatch.setattr(ui_param_cache, "PC", False)
monkeypatch.setattr(ui_param_cache.os, "SCHED_OTHER", 0, raising=False)
monkeypatch.setattr(ui_param_cache.os, "sched_param", lambda priority: priority, raising=False)
monkeypatch.setattr(ui_param_cache.os, "sched_setscheduler", set_scheduler, raising=False)
monkeypatch.setattr(ui_param_cache.cloudlog, "exception", lambda *_: None)
params = FakeParams()
now = [0.0]
cache = UIParamCache(params, clock=lambda: now[0])
assert cache.get_int("count") == 1
cache.start()
worker = cache._worker
if worker is not None:
worker.join(1)
assert not worker.is_alive()
params.values["count"] = 2
now[0] = 0.2
assert cache.get_int("count") == 2
assert cache._worker is None