IQ.Pilot Prebuilt Release @ 27f668a
This commit is contained in:
9
iqpilot/selfdrive/ui/tests/.gitignore
vendored
Normal file
9
iqpilot/selfdrive/ui/tests/.gitignore
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
test
|
||||
test_translations
|
||||
test_ui/report_1
|
||||
test_ui/raylib_report
|
||||
|
||||
diff/*.mp4
|
||||
diff/*.html
|
||||
diff/.coverage
|
||||
diff/htmlcov/
|
||||
0
iqpilot/selfdrive/ui/tests/__init__.py
Normal file
0
iqpilot/selfdrive/ui/tests/__init__.py
Normal file
35
iqpilot/selfdrive/ui/tests/cycle_offroad_alerts.py
Executable file
35
iqpilot/selfdrive/ui/tests/cycle_offroad_alerts.py
Executable file
@@ -0,0 +1,35 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import json
|
||||
|
||||
from iqpilot.common.basedir import BASEDIR
|
||||
from iqpilot.common.params import Params
|
||||
from iqpilot.selfdrive.selfdrived.alertmanager import set_offroad_alert
|
||||
from iqpilot.system.updated.updated import parse_release_notes
|
||||
|
||||
if __name__ == "__main__":
|
||||
params = Params()
|
||||
|
||||
with open(os.path.join(BASEDIR, "iqpilot/selfdrive/selfdrived/alerts_offroad.json")) as f:
|
||||
offroad_alerts = json.load(f)
|
||||
|
||||
t = 10 if len(sys.argv) < 2 else int(sys.argv[1])
|
||||
while True:
|
||||
print("setting alert update")
|
||||
params.put_bool("UpdateAvailable", True)
|
||||
params.put("UpdaterNewReleaseNotes", parse_release_notes(BASEDIR))
|
||||
|
||||
time.sleep(t)
|
||||
params.put_bool("UpdateAvailable", False)
|
||||
|
||||
# cycle through normal alerts
|
||||
for a in offroad_alerts:
|
||||
print("setting alert:", a)
|
||||
set_offroad_alert(a, True)
|
||||
time.sleep(t)
|
||||
set_offroad_alert(a, False)
|
||||
|
||||
print("no alert")
|
||||
time.sleep(t)
|
||||
23
iqpilot/selfdrive/ui/tests/test_feedbackd.py
Normal file
23
iqpilot/selfdrive/ui/tests/test_feedbackd.py
Normal file
@@ -0,0 +1,23 @@
|
||||
import time
|
||||
|
||||
import pytest
|
||||
|
||||
import iqpilot.cereal.messaging as messaging
|
||||
from iqpilot.system.manager.process_config import managed_processes
|
||||
|
||||
|
||||
@pytest.mark.linux
|
||||
def test_feedbackd_publishes_bookmark():
|
||||
publisher = messaging.PubMaster(["bookmarkButton"])
|
||||
subscriber = messaging.SubMaster(["userBookmark"])
|
||||
process = managed_processes["feedbackd"]
|
||||
process.start()
|
||||
try:
|
||||
assert publisher.wait_for_readers_to_update("bookmarkButton", timeout=5)
|
||||
deadline = time.monotonic() + 5
|
||||
while time.monotonic() < deadline and not subscriber.updated["userBookmark"]:
|
||||
publisher.send("bookmarkButton", messaging.new_message("bookmarkButton"))
|
||||
subscriber.update(100)
|
||||
assert subscriber.updated["userBookmark"]
|
||||
finally:
|
||||
process.stop()
|
||||
38
iqpilot/selfdrive/ui/tests/test_local_routes.py
Normal file
38
iqpilot/selfdrive/ui/tests/test_local_routes.py
Normal file
@@ -0,0 +1,38 @@
|
||||
from pathlib import Path
|
||||
|
||||
from iqpilot.selfdrive.ui.lib.local_routes import list_local_routes
|
||||
|
||||
|
||||
def _touch(path: Path) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_bytes(b"")
|
||||
|
||||
|
||||
def test_list_local_routes_from_segment_directories(tmp_path):
|
||||
route_name = "00000051--3141cf1d76"
|
||||
_touch(tmp_path / f"{route_name}--0" / "qcamera.ts")
|
||||
_touch(tmp_path / f"{route_name}--1" / "qlog.zst")
|
||||
|
||||
routes = list_local_routes(tmp_path)
|
||||
|
||||
assert len(routes) == 1
|
||||
assert routes[0].name == route_name
|
||||
assert routes[0].segment_count == 2
|
||||
assert routes[0].cameras == ("road",)
|
||||
|
||||
|
||||
def test_list_local_routes_from_single_segment_directory(tmp_path):
|
||||
route_name = "00000052--3141cf1d77"
|
||||
_touch(tmp_path / f"{route_name}--0" / "qcamera.ts")
|
||||
|
||||
routes = list_local_routes(tmp_path)
|
||||
|
||||
assert len(routes) == 1
|
||||
assert routes[0].name == route_name
|
||||
assert routes[0].subtitle == "1:00 · Road Cam"
|
||||
|
||||
|
||||
def test_list_local_routes_ignores_invalid_entries(tmp_path):
|
||||
_touch(tmp_path / "not-a-route" / "fcamera.hevc")
|
||||
|
||||
assert list_local_routes(tmp_path) == []
|
||||
71
iqpilot/selfdrive/ui/tests/test_nav_helpers.py
Normal file
71
iqpilot/selfdrive/ui/tests/test_nav_helpers.py
Normal file
@@ -0,0 +1,71 @@
|
||||
import json
|
||||
|
||||
from iqpilot.common.params import Params
|
||||
from iqpilot.selfdrive.ui.lib.nav_helpers import current_or_last_gps_position, resolve_mapbox_token
|
||||
|
||||
|
||||
def test_resolve_mapbox_token_reads_param(tmp_path):
|
||||
params = Params(tmp_path.as_posix())
|
||||
params.put("MapboxToken", "pk.test-token")
|
||||
|
||||
assert resolve_mapbox_token(params) == "pk.test-token"
|
||||
|
||||
|
||||
def test_resolve_mapbox_token_missing_returns_empty(tmp_path):
|
||||
params = Params(tmp_path.as_posix())
|
||||
|
||||
assert resolve_mapbox_token(params) == ""
|
||||
|
||||
|
||||
def test_current_or_last_gps_position_uses_last_position_param(tmp_path):
|
||||
params = Params(tmp_path.as_posix())
|
||||
params.put("LastGPSPosition", json.dumps({
|
||||
"latitude": 37.7749,
|
||||
"longitude": -122.4194,
|
||||
"bearing": 91.5,
|
||||
}))
|
||||
|
||||
lat, lon, bearing, valid = current_or_last_gps_position(params)
|
||||
|
||||
assert valid
|
||||
assert lat == 37.7749
|
||||
assert lon == -122.4194
|
||||
assert bearing == 91.5
|
||||
|
||||
|
||||
def test_current_or_last_gps_position_uses_iqloc_position_param(tmp_path):
|
||||
params = Params(tmp_path.as_posix())
|
||||
params.put("LastGPSPositionIQLoc", json.dumps({
|
||||
"latitude": 34.0522,
|
||||
"longitude": -118.2437,
|
||||
"bearingDeg": 12.0,
|
||||
}))
|
||||
|
||||
lat, lon, bearing, valid = current_or_last_gps_position(params)
|
||||
|
||||
assert valid
|
||||
assert lat == 34.0522
|
||||
assert lon == -118.2437
|
||||
assert bearing == 12.0
|
||||
|
||||
|
||||
def test_current_or_last_gps_position_accepts_lat_lon_aliases(tmp_path):
|
||||
params = Params(tmp_path.as_posix())
|
||||
params.put("LastGPSPosition", json.dumps({
|
||||
"lat": 40.7128,
|
||||
"lng": -74.006,
|
||||
}))
|
||||
|
||||
lat, lon, bearing, valid = current_or_last_gps_position(params)
|
||||
|
||||
assert valid
|
||||
assert lat == 40.7128
|
||||
assert lon == -74.006
|
||||
assert bearing == 0.0
|
||||
|
||||
|
||||
def test_current_or_last_gps_position_rejects_zero_position(tmp_path):
|
||||
params = Params(tmp_path.as_posix())
|
||||
params.put("LastGPSPosition", "{}")
|
||||
|
||||
assert current_or_last_gps_position(params) == (0.0, 0.0, 0.0, False)
|
||||
72
iqpilot/selfdrive/ui/tests/test_nav_map_utils.py
Normal file
72
iqpilot/selfdrive/ui/tests/test_nav_map_utils.py
Normal file
@@ -0,0 +1,72 @@
|
||||
from types import SimpleNamespace
|
||||
|
||||
from iqpilot.ui.onroad.nav_map_utils import (
|
||||
build_mapbox_static_url,
|
||||
build_mapbox_tile_url,
|
||||
choose_nav_camera,
|
||||
mercator_world_px,
|
||||
mercator_world_px_at_zoom,
|
||||
project_nav_point,
|
||||
project_nav_polyline,
|
||||
tile_world_size,
|
||||
)
|
||||
|
||||
|
||||
def test_mercator_world_px_changes_with_longitude():
|
||||
x1, y1 = mercator_world_px(41.8826, -87.6393, 16.0)
|
||||
x2, y2 = mercator_world_px(41.8826, -87.6293, 16.0)
|
||||
|
||||
assert x2 > x1
|
||||
assert abs(y2 - y1) < 1.0
|
||||
|
||||
|
||||
def test_project_nav_point_centers_current_position():
|
||||
x, y = project_nav_point(41.8826, -87.6393, 41.8826, -87.6393, 16.0, 90.0, 420.0, 420.0)
|
||||
|
||||
assert round(x, 3) == 210.0
|
||||
assert round(y, 3) == 210.0
|
||||
|
||||
|
||||
def test_project_nav_polyline_preserves_point_count():
|
||||
points = [
|
||||
SimpleNamespace(latitude=41.8826, longitude=-87.6422),
|
||||
SimpleNamespace(latitude=41.8826, longitude=-87.6393),
|
||||
SimpleNamespace(latitude=41.8830, longitude=-87.6366),
|
||||
]
|
||||
|
||||
projected = project_nav_polyline(points, 41.8826, -87.6393, 16.0, 90.0, 420.0, 420.0)
|
||||
|
||||
assert len(projected) == len(points)
|
||||
|
||||
|
||||
def test_choose_nav_camera_looks_ahead_of_vehicle():
|
||||
points = [
|
||||
SimpleNamespace(latitude=41.8826, longitude=-87.6393),
|
||||
SimpleNamespace(latitude=41.8835, longitude=-87.6355),
|
||||
]
|
||||
|
||||
center_lat, center_lon, zoom = choose_nav_camera(41.8826, -87.6393, 90.0, points, 420.0, 420.0, 16.2)
|
||||
|
||||
assert center_lon > -87.6393
|
||||
assert 16.0 <= zoom <= 17.8
|
||||
|
||||
|
||||
def test_build_mapbox_static_url_contains_expected_components():
|
||||
url = build_mapbox_static_url(41.8826, -87.6393, 16.2, 90.0, 420, 420)
|
||||
|
||||
assert "navigation-night-v1/static/" in url
|
||||
assert "-87.639300,41.882600,16.20,90.0,0/420x420@2x" in url
|
||||
|
||||
|
||||
def test_build_mapbox_tile_url_contains_expected_components():
|
||||
url = build_mapbox_tile_url(16, 10619, 24322)
|
||||
|
||||
assert "navigation-night-v1/tiles/256/16/10619/24322@2x" in url
|
||||
|
||||
|
||||
def test_world_size_and_world_px_align_at_integer_zoom():
|
||||
world_size = tile_world_size(16)
|
||||
x, y = mercator_world_px_at_zoom(41.8826, -87.6393, 16)
|
||||
|
||||
assert 0.0 <= x <= world_size
|
||||
assert 0.0 <= y <= world_size
|
||||
95
iqpilot/selfdrive/ui/tests/test_nav_search.py
Normal file
95
iqpilot/selfdrive/ui/tests/test_nav_search.py
Normal file
@@ -0,0 +1,95 @@
|
||||
"""
|
||||
Copyright (c) IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos
|
||||
"""
|
||||
from types import SimpleNamespace
|
||||
|
||||
from iqpilot.selfdrive.ui.lib import nav_search
|
||||
|
||||
|
||||
class FakeParams:
|
||||
def get(self, key, *args, **kwargs):
|
||||
return {
|
||||
"AmapWebServiceKey": "amap-key",
|
||||
"OsmLocationName": "CN",
|
||||
"MapboxToken": "mapbox-key",
|
||||
}.get(key)
|
||||
|
||||
|
||||
def test_china_search_uses_amap_without_calling_mapbox(monkeypatch):
|
||||
search = nav_search.NavSearch.__new__(nav_search.NavSearch)
|
||||
search._params = FakeParams()
|
||||
search._seq = 1
|
||||
search._results = []
|
||||
search._searching = True
|
||||
search._amap_adcode = ""
|
||||
import threading
|
||||
search._lock = threading.Lock()
|
||||
|
||||
amap_client = SimpleNamespace(
|
||||
is_mainland_china_configured=lambda *args, **kwargs: True,
|
||||
get_key=lambda *args, **kwargs: "amap-key",
|
||||
status=lambda: "ok",
|
||||
reverse_adcode=lambda *args, **kwargs: "",
|
||||
autocomplete=lambda *args, **kwargs: [],
|
||||
)
|
||||
monkeypatch.setattr(nav_search, "_amap_client", amap_client)
|
||||
monkeypatch.setattr(nav_search, "current_or_last_gps_position", lambda *_: (39.9, 116.4, 0.0, True))
|
||||
monkeypatch.setattr(amap_client, "reverse_adcode", lambda *args, **kwargs: "110000")
|
||||
monkeypatch.setattr(amap_client, "autocomplete", lambda *args, **kwargs: [
|
||||
SimpleNamespace(
|
||||
name="Tiananmen",
|
||||
address="Dongcheng, Beijing",
|
||||
provider_id="B000A83M61",
|
||||
latitude=39.9087,
|
||||
longitude=116.3975,
|
||||
),
|
||||
])
|
||||
monkeypatch.setattr(nav_search.requests, "get", lambda *args, **kwargs: (_ for _ in ()).throw(AssertionError("Mapbox called")))
|
||||
|
||||
search._do_search("Tiananmen", 1)
|
||||
|
||||
assert len(search._results) == 1
|
||||
assert search._results[0].provider == "amap"
|
||||
assert search._results[0].has_coords
|
||||
|
||||
|
||||
def test_non_china_search_preserves_mapbox_request(monkeypatch):
|
||||
search = nav_search.NavSearch.__new__(nav_search.NavSearch)
|
||||
search._params = FakeParams()
|
||||
search._session = "session"
|
||||
search._seq = 1
|
||||
search._results = []
|
||||
search._searching = True
|
||||
search._amap_adcode = ""
|
||||
import threading
|
||||
search._lock = threading.Lock()
|
||||
captured = {}
|
||||
|
||||
class Response:
|
||||
def raise_for_status(self):
|
||||
return None
|
||||
|
||||
def json(self):
|
||||
return {"suggestions": []}
|
||||
|
||||
def get(url, *, params, timeout):
|
||||
captured.update(url=url, params=params, timeout=timeout)
|
||||
return Response()
|
||||
|
||||
monkeypatch.setattr(nav_search, "current_or_last_gps_position", lambda *_: (41.3, -90.2, 0.0, True))
|
||||
monkeypatch.setattr(nav_search, "resolve_mapbox_token", lambda *_: "mapbox-key")
|
||||
monkeypatch.setattr(nav_search, "_amap_client", None)
|
||||
monkeypatch.setattr(nav_search.requests, "get", get)
|
||||
|
||||
search._do_search("Home", 1)
|
||||
|
||||
assert captured["url"] == f"{nav_search.SEARCHBOX}/suggest"
|
||||
assert captured["params"] == {
|
||||
"q": "Home",
|
||||
"access_token": "mapbox-key",
|
||||
"session_token": "session",
|
||||
"limit": nav_search.MAX_RESULTS,
|
||||
"language": "en",
|
||||
"proximity": "-90.2,41.3",
|
||||
}
|
||||
assert captured["timeout"] == 8
|
||||
39
iqpilot/selfdrive/ui/tests/test_offline_raster_pipeline.py
Normal file
39
iqpilot/selfdrive/ui/tests/test_offline_raster_pipeline.py
Normal file
@@ -0,0 +1,39 @@
|
||||
import sqlite3
|
||||
|
||||
from scripts.iqpilot.package_xyz_tiles_to_mbtiles import build_mbtiles
|
||||
from scripts.iqpilot.render_raster_tiles_from_vector_mbtiles import tile_range_for_bounds
|
||||
|
||||
|
||||
PNG_1X1 = (
|
||||
b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01"
|
||||
b"\x08\x06\x00\x00\x00\x1f\x15\xc4\x89\x00\x00\x00\rIDATx\x9cc\xf8\xcf"
|
||||
b"\xc0\xf0\x1f\x00\x05\x00\x01\xff\x89\x99=\x1d\x00\x00\x00\x00IEND\xaeB`\x82"
|
||||
)
|
||||
|
||||
|
||||
def test_tile_range_for_bounds_returns_non_empty_ranges():
|
||||
x_range, y_range = tile_range_for_bounds((-88.15, 41.65, -88.02, 41.76), 14)
|
||||
assert len(list(x_range)) > 0
|
||||
assert len(list(y_range)) > 0
|
||||
|
||||
|
||||
def test_build_mbtiles_from_xyz_tiles(tmp_path):
|
||||
source = tmp_path / "xyz"
|
||||
tile_dir = source / "14" / "2625"
|
||||
tile_dir.mkdir(parents=True)
|
||||
(tile_dir / "6335@2x.png").write_bytes(PNG_1X1)
|
||||
|
||||
output = tmp_path / "offline.mbtiles"
|
||||
build_mbtiles(source, output, bounds="-88.15,41.65,-88.02,41.76")
|
||||
|
||||
conn = sqlite3.connect(output)
|
||||
try:
|
||||
fmt = conn.execute("SELECT value FROM metadata WHERE name='format'").fetchone()[0]
|
||||
bounds = conn.execute("SELECT value FROM metadata WHERE name='bounds'").fetchone()[0]
|
||||
count = conn.execute("SELECT COUNT(*) FROM tiles").fetchone()[0]
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
assert fmt == "png"
|
||||
assert bounds == "-88.15,41.65,-88.02,41.76"
|
||||
assert count == 1
|
||||
128
iqpilot/selfdrive/ui/tests/test_offline_tiles.py
Normal file
128
iqpilot/selfdrive/ui/tests/test_offline_tiles.py
Normal file
@@ -0,0 +1,128 @@
|
||||
import os
|
||||
import sqlite3
|
||||
import json
|
||||
|
||||
from iqpilot.ui.onroad import offline_tiles
|
||||
|
||||
|
||||
PNG_1X1 = (
|
||||
b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01"
|
||||
b"\x08\x06\x00\x00\x00\x1f\x15\xc4\x89\x00\x00\x00\rIDATx\x9cc\xf8\xcf"
|
||||
b"\xc0\xf0\x1f\x00\x05\x00\x01\xff\x89\x99=\x1d\x00\x00\x00\x00IEND\xaeB`\x82"
|
||||
)
|
||||
|
||||
|
||||
def _write_mbtiles(path):
|
||||
conn = sqlite3.connect(path)
|
||||
conn.execute("CREATE TABLE metadata (name text, value text)")
|
||||
conn.execute("CREATE TABLE tiles (zoom_level integer, tile_column integer, tile_row integer, tile_data blob)")
|
||||
conn.execute("INSERT INTO metadata (name, value) VALUES ('format', 'png')")
|
||||
conn.execute("INSERT INTO metadata (name, value) VALUES ('minzoom', '1')")
|
||||
conn.execute("INSERT INTO metadata (name, value) VALUES ('maxzoom', '3')")
|
||||
conn.execute(
|
||||
"INSERT INTO tiles (zoom_level, tile_column, tile_row, tile_data) VALUES (?, ?, ?, ?)",
|
||||
(1, 1, offline_tiles.xyz_to_tms_y(1, 0), PNG_1X1),
|
||||
)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
|
||||
def test_xyz_to_tms_y():
|
||||
assert offline_tiles.xyz_to_tms_y(1, 0) == 1
|
||||
assert offline_tiles.xyz_to_tms_y(1, 1) == 0
|
||||
|
||||
|
||||
def test_find_offline_mbtiles_path_from_env(tmp_path, monkeypatch):
|
||||
mbtiles = tmp_path / "demo.mbtiles"
|
||||
_write_mbtiles(mbtiles)
|
||||
monkeypatch.setenv(offline_tiles.OFFLINE_MBTILES_ENV, str(mbtiles))
|
||||
assert offline_tiles.find_offline_mbtiles_path() == mbtiles
|
||||
|
||||
|
||||
def test_find_offline_xyz_root(tmp_path, monkeypatch):
|
||||
root = tmp_path / "tiles"
|
||||
(root / "15" / "10500").mkdir(parents=True)
|
||||
monkeypatch.setenv(offline_tiles.OFFLINE_TILE_ROOT_ENV, str(root))
|
||||
assert offline_tiles.find_offline_xyz_root() == root
|
||||
|
||||
|
||||
def test_load_raster_tile_blob_from_mbtiles(tmp_path):
|
||||
mbtiles = tmp_path / "offline.mbtiles"
|
||||
_write_mbtiles(mbtiles)
|
||||
conn = offline_tiles.open_mbtiles(mbtiles)
|
||||
try:
|
||||
assert offline_tiles.mbtiles_is_raster(conn) is True
|
||||
assert offline_tiles.mbtiles_zoom_bounds(conn) == (1, 3)
|
||||
assert offline_tiles.load_raster_tile_blob(conn, 1, 1, 0) == PNG_1X1
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def test_load_raster_tile_blob_from_xyz_dir(tmp_path, monkeypatch):
|
||||
root = tmp_path / "offline_tiles"
|
||||
tile_path = root / "14" / "2625"
|
||||
tile_path.mkdir(parents=True)
|
||||
(tile_path / "6335@2x.png").write_bytes(PNG_1X1)
|
||||
monkeypatch.setenv(offline_tiles.OFFLINE_TILE_ROOT_ENV, str(root))
|
||||
assert offline_tiles.xyz_zoom_bounds(root) == (14, 14)
|
||||
assert offline_tiles.load_raster_xyz_tile_blob(root, 14, 2625, 6335) == PNG_1X1
|
||||
|
||||
|
||||
def test_find_offline_region_root_by_bounds(tmp_path, monkeypatch):
|
||||
root = tmp_path / "offline_maps"
|
||||
il = root / "regions" / "illinois"
|
||||
ca = root / "regions" / "california"
|
||||
(il / "tiles").mkdir(parents=True)
|
||||
(ca / "tiles").mkdir(parents=True)
|
||||
(il / "manifest.json").write_text(json.dumps({"mbtiles": {"bounds": "-91.6,36.9,-87.4,42.6"}}))
|
||||
(ca / "manifest.json").write_text(json.dumps({"mbtiles": {"bounds": "-124.5,32.4,-114.1,42.1"}}))
|
||||
|
||||
monkeypatch.setenv(offline_tiles.OFFLINE_TILE_ROOT_ENV, str(root / "tiles"))
|
||||
assert offline_tiles.find_offline_region_root(41.88, -87.63) == il
|
||||
assert offline_tiles.find_offline_region_root(34.05, -118.24) == ca
|
||||
|
||||
|
||||
def test_find_offline_mbtiles_path_uses_selected_region(tmp_path, monkeypatch):
|
||||
root = tmp_path / "offline_maps"
|
||||
il = root / "regions" / "illinois"
|
||||
ca = root / "regions" / "california"
|
||||
(il / "tiles").mkdir(parents=True)
|
||||
(ca / "tiles").mkdir(parents=True)
|
||||
(il / "manifest.json").write_text(json.dumps({"mbtiles": {"bounds": "-91.6,36.9,-87.4,42.6"}}))
|
||||
(ca / "manifest.json").write_text(json.dumps({"mbtiles": {"bounds": "-124.5,32.4,-114.1,42.1"}}))
|
||||
_write_mbtiles(il / "tiles" / "offline.mbtiles")
|
||||
_write_mbtiles(ca / "tiles" / "offline.mbtiles")
|
||||
|
||||
monkeypatch.setenv(offline_tiles.OFFLINE_TILE_ROOT_ENV, str(root / "tiles"))
|
||||
assert offline_tiles.find_offline_mbtiles_path(41.88, -87.63) == il / "tiles" / "offline.mbtiles"
|
||||
assert offline_tiles.find_offline_mbtiles_path(34.05, -118.24) == ca / "tiles" / "offline.mbtiles"
|
||||
|
||||
|
||||
def test_day_variant_selection(tmp_path, monkeypatch):
|
||||
root = tmp_path / "offline_maps"
|
||||
region = root / "regions" / "us_state.IL"
|
||||
(region / "tiles").mkdir(parents=True)
|
||||
(region / "manifest.json").write_text(json.dumps({"mbtiles": {"bounds": "-88.3,41.5,-87.8,41.9"}}))
|
||||
_write_mbtiles(region / "tiles" / "offline.mbtiles")
|
||||
monkeypatch.setenv(offline_tiles.OFFLINE_TILE_ROOT_ENV, str(root / "tiles"))
|
||||
offline_tiles._region_roots_cache = None
|
||||
offline_tiles._region_bounds_cache.clear()
|
||||
|
||||
# no day variant yet: day request falls back to the night set
|
||||
night = region / "tiles" / "offline.mbtiles"
|
||||
assert offline_tiles.find_offline_mbtiles_path(41.7, -88.0, day=True) == night
|
||||
assert offline_tiles.find_offline_mbtiles_path(41.7, -88.0, day=False) == night
|
||||
|
||||
# day variant installed: day requests prefer it, night unchanged
|
||||
_write_mbtiles(region / "tiles" / "offline_day.mbtiles")
|
||||
assert offline_tiles.find_offline_mbtiles_path(41.7, -88.0, day=True) == region / "tiles" / "offline_day.mbtiles"
|
||||
assert offline_tiles.find_offline_mbtiles_path(41.7, -88.0, day=False) == night
|
||||
|
||||
|
||||
def test_solar_elevation_day_night():
|
||||
from iqpilot.ui.onroad.nav_map_utils import solar_elevation_deg
|
||||
# Chicago 2026-07-11: 18:00 UTC (1pm CDT) is day; 06:00 UTC (1am CDT) is night
|
||||
noon_utc = 1783792800.0 # 2026-07-11 18:00:00 UTC
|
||||
night_utc = noon_utc - 12 * 3600
|
||||
assert solar_elevation_deg(41.88, -87.63, noon_utc) > 30.0
|
||||
assert solar_elevation_deg(41.88, -87.63, night_utc) < -10.0
|
||||
27
iqpilot/selfdrive/ui/tests/test_raylib_ui.py
Normal file
27
iqpilot/selfdrive/ui/tests/test_raylib_ui.py
Normal file
@@ -0,0 +1,27 @@
|
||||
import time
|
||||
import pytest
|
||||
from iqpilot.selfdrive.test.helpers import with_processes
|
||||
from iqpilot.system.ui.lib import application
|
||||
from iqpilot.system.ui.lib.utils import gui_style_color
|
||||
|
||||
|
||||
@pytest.mark.linux
|
||||
@with_processes(["ui"])
|
||||
def test_raylib_ui():
|
||||
"""Test initialization of the UI widgets is successful."""
|
||||
time.sleep(1)
|
||||
|
||||
|
||||
def test_style_colors_match_gui_style_abi(monkeypatch):
|
||||
calls = []
|
||||
monkeypatch.setattr(application.rl, "gui_set_style", lambda *args: calls.append(args))
|
||||
monkeypatch.setattr(application, "gui_style_color", lambda color: 27)
|
||||
application.GuiApplication._set_styles(None)
|
||||
assert [call[2] for call in calls[-3:]] == [27, 27, 27]
|
||||
|
||||
|
||||
def test_gui_style_color_uses_binding_value_type():
|
||||
color = application.rl.Color(229, 229, 229, 255)
|
||||
value_type = application.rl.ffi.typeof(application.rl.raylib.GuiSetStyle).args[2]
|
||||
expected = int(application.rl.ffi.cast(value_type, application.rl.color_to_int(color)))
|
||||
assert gui_style_color(color) == expected
|
||||
@@ -0,0 +1,56 @@
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
||||
from iqpilot.selfdrive.ui.layouts.main import MainLayout, MainState
|
||||
|
||||
|
||||
def make_layout(current_mode):
|
||||
layout = object.__new__(MainLayout)
|
||||
layout._current_mode = current_mode
|
||||
layout._set_mode_calls = []
|
||||
layout._set_mode_for_state = lambda: layout._set_mode_calls.append(current_mode)
|
||||
return layout
|
||||
|
||||
|
||||
class FakeSm:
|
||||
def __init__(self, v_ego, carstate_valid):
|
||||
self.valid = {"carState": carstate_valid}
|
||||
self._v_ego = v_ego
|
||||
|
||||
def __getitem__(self, key):
|
||||
return SimpleNamespace(vEgo=self._v_ego)
|
||||
|
||||
|
||||
class TestSettingsInteractiveTimeout:
|
||||
def _run(self, current_mode, started, v_ego, carstate_valid=True):
|
||||
layout = make_layout(current_mode)
|
||||
fake = SimpleNamespace(started=started, sm=FakeSm(v_ego, carstate_valid))
|
||||
monkeypatch = pytest.MonkeyPatch()
|
||||
monkeypatch.setattr("iqpilot.selfdrive.ui.layouts.main.ui_state", fake)
|
||||
try:
|
||||
layout._on_interactive_timeout()
|
||||
finally:
|
||||
monkeypatch.undo()
|
||||
return layout._set_mode_calls
|
||||
|
||||
def test_stationary_in_settings_stays(self):
|
||||
# parked/charging hybrid reads onroad; the timeout must not eject from Settings
|
||||
assert self._run(MainState.SETTINGS, started=True, v_ego=0.0) == []
|
||||
|
||||
def test_moving_in_settings_returns_to_road(self):
|
||||
assert self._run(MainState.SETTINGS, started=True, v_ego=5.0) == [MainState.SETTINGS]
|
||||
|
||||
def test_onroad_layout_always_handled(self):
|
||||
assert self._run(MainState.ONROAD, started=True, v_ego=0.0) == [MainState.ONROAD]
|
||||
|
||||
def test_home_layout_always_handled(self):
|
||||
assert self._run(MainState.HOME, started=True, v_ego=0.0) == [MainState.HOME]
|
||||
|
||||
def test_offroad_in_settings_handled(self):
|
||||
# car off (offroad): existing behavior is unchanged
|
||||
assert self._run(MainState.SETTINGS, started=False, v_ego=0.0) == [MainState.SETTINGS]
|
||||
|
||||
def test_invalid_carstate_treated_as_moving(self):
|
||||
# if speed is unknown, fail safe to the road view rather than trapping in settings
|
||||
assert self._run(MainState.SETTINGS, started=True, v_ego=0.0, carstate_valid=False) == [MainState.SETTINGS]
|
||||
34
iqpilot/selfdrive/ui/tests/test_soundd.py
Normal file
34
iqpilot/selfdrive/ui/tests/test_soundd.py
Normal file
@@ -0,0 +1,34 @@
|
||||
from types import SimpleNamespace
|
||||
|
||||
from iqpilot.cereal import car
|
||||
from iqpilot.selfdrive.ui import soundd
|
||||
|
||||
AudibleAlert = car.CarControl.HUDControl.AudibleAlert
|
||||
|
||||
|
||||
class TestSoundd:
|
||||
def test_check_selfdrive_timeout_alert(self, monkeypatch):
|
||||
class FakeSubMaster:
|
||||
recv_time = {'selfdriveState': 100.0}
|
||||
|
||||
def __init__(self, enabled):
|
||||
self.state = SimpleNamespace(enabled=enabled)
|
||||
|
||||
def __getitem__(self, service):
|
||||
assert service == 'selfdriveState'
|
||||
return self.state
|
||||
|
||||
enabled = FakeSubMaster(True)
|
||||
disabled = FakeSubMaster(False)
|
||||
|
||||
monkeypatch.setattr(soundd.time, "monotonic", lambda: 100.0 + soundd.SELFDRIVE_STATE_TIMEOUT)
|
||||
assert not soundd.check_selfdrive_timeout_alert(enabled)
|
||||
|
||||
monkeypatch.setattr(soundd.time, "monotonic", lambda: 101.0 + soundd.SELFDRIVE_STATE_TIMEOUT)
|
||||
assert soundd.check_selfdrive_timeout_alert(enabled)
|
||||
assert not soundd.check_selfdrive_timeout_alert(disabled)
|
||||
|
||||
monkeypatch.setattr(soundd.time, "monotonic", lambda: 110.0 + soundd.SELFDRIVE_STATE_TIMEOUT)
|
||||
assert not soundd.check_selfdrive_timeout_alert(enabled)
|
||||
|
||||
# TODO: add test with micd for checking that soundd actually outputs sounds
|
||||
48
iqpilot/selfdrive/ui/tests/test_translations.py
Normal file
48
iqpilot/selfdrive/ui/tests/test_translations.py
Normal file
@@ -0,0 +1,48 @@
|
||||
import gettext
|
||||
import json
|
||||
import re
|
||||
|
||||
import pytest
|
||||
|
||||
from iqpilot.system.ui.lib.multilang import LANGUAGES_FILE, TRANSLATIONS_DIR
|
||||
|
||||
|
||||
FORMAT_ARG = re.compile(r"%(?:\([^)]+\))?[#0+\-]?(?:\d+|\*)?(?:\.\d+|\.\*)?[hlL]?[diouxXeEfFgGcrsa%]")
|
||||
|
||||
|
||||
with LANGUAGES_FILE.open(encoding="utf-8") as stream:
|
||||
LANGUAGES = json.load(stream)
|
||||
|
||||
|
||||
def load_catalog(language_code: str) -> dict[str | tuple[str, int], str]:
|
||||
with TRANSLATIONS_DIR.joinpath(f"app_{language_code}.mo").open("rb") as stream:
|
||||
return gettext.GNUTranslations(stream)._catalog
|
||||
|
||||
|
||||
def message_keys(catalog: dict[str | tuple[str, int], str]) -> set[str]:
|
||||
return {key for key in catalog if isinstance(key, str) and key}
|
||||
|
||||
|
||||
def format_args(text: str) -> list[str]:
|
||||
return sorted(match for match in FORMAT_ARG.findall(text) if match != "%%")
|
||||
|
||||
|
||||
def test_language_codes_are_unique():
|
||||
assert len(LANGUAGES) == len(set(LANGUAGES.values()))
|
||||
|
||||
|
||||
@pytest.mark.parametrize("language_code", LANGUAGES.values(), ids=LANGUAGES.keys())
|
||||
def test_translation_catalog_is_complete(language_code):
|
||||
source = load_catalog("en")
|
||||
translated = load_catalog(language_code)
|
||||
assert message_keys(translated) == message_keys(source)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("language_code", LANGUAGES.values(), ids=LANGUAGES.keys())
|
||||
def test_translation_catalog_entries(language_code):
|
||||
catalog = load_catalog(language_code)
|
||||
for source, translated in catalog.items():
|
||||
if not isinstance(source, str) or not source:
|
||||
continue
|
||||
assert translated
|
||||
assert format_args(translated) == format_args(source)
|
||||
Reference in New Issue
Block a user