IQ.Pilot Release Commit @ a209cd3
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)
|
||||
53
iqpilot/selfdrive/ui/tests/test_feedbackd.py
Normal file
53
iqpilot/selfdrive/ui/tests/test_feedbackd.py
Normal file
@@ -0,0 +1,53 @@
|
||||
import pytest
|
||||
import iqpilot.cereal.messaging as messaging
|
||||
from iqpilot.cereal import car
|
||||
from iqpilot.common.params import Params
|
||||
from iqpilot.system.manager.process_config import managed_processes
|
||||
|
||||
|
||||
@pytest.mark.skip("tmp disabled")
|
||||
class TestFeedbackd:
|
||||
def setup_method(self):
|
||||
self.pm = messaging.PubMaster(['carState', 'rawAudioData'])
|
||||
self.sm = messaging.SubMaster(['audioFeedback'])
|
||||
|
||||
def _send_lkas_button(self, pressed: bool):
|
||||
msg = messaging.new_message('carState')
|
||||
msg.carState.canValid = True
|
||||
msg.carState.buttonEvents = [{'type': car.CarState.ButtonEvent.Type.lkas, 'pressed': pressed}]
|
||||
self.pm.send('carState', msg)
|
||||
|
||||
def _send_audio_data(self, count: int = 5):
|
||||
for _ in range(count):
|
||||
audio_msg = messaging.new_message('rawAudioData')
|
||||
audio_msg.rawAudioData.data = bytes(1600) # 800 samples of int16
|
||||
audio_msg.rawAudioData.sampleRate = 16000
|
||||
self.pm.send('rawAudioData', audio_msg)
|
||||
self.sm.update(timeout=100)
|
||||
|
||||
@pytest.mark.parametrize("record_feedback", [False, True])
|
||||
def test_audio_feedback(self, record_feedback):
|
||||
Params().put_bool("RecordAudioFeedback", record_feedback)
|
||||
|
||||
managed_processes["feedbackd"].start()
|
||||
assert self.pm.wait_for_readers_to_update('carState', timeout=5)
|
||||
assert self.pm.wait_for_readers_to_update('rawAudioData', timeout=5)
|
||||
|
||||
self._send_lkas_button(pressed=True)
|
||||
self._send_audio_data()
|
||||
self._send_lkas_button(pressed=False)
|
||||
self._send_audio_data()
|
||||
|
||||
if record_feedback:
|
||||
assert self.sm.updated['audioFeedback'], "audioFeedback should be published when enabled"
|
||||
else:
|
||||
assert not self.sm.updated['audioFeedback'], "audioFeedback should not be published when disabled"
|
||||
|
||||
self._send_lkas_button(pressed=True)
|
||||
self._send_audio_data()
|
||||
self._send_lkas_button(pressed=False)
|
||||
self._send_audio_data()
|
||||
|
||||
assert not self.sm.updated['audioFeedback'], "audioFeedback should not be published after second press"
|
||||
|
||||
managed_processes["feedbackd"].stop()
|
||||
39
iqpilot/selfdrive/ui/tests/test_local_routes.py
Normal file
39
iqpilot/selfdrive/ui/tests/test_local_routes.py
Normal file
@@ -0,0 +1,39 @@
|
||||
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 = "aaaaaaaaaaaaaaaa|2026-07-03--12-30-00"
|
||||
_touch(tmp_path / f"{route_name}--0" / "fcamera.hevc")
|
||||
_touch(tmp_path / f"{route_name}--1" / "rlog.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].camera_count == 1
|
||||
assert "Jul 3" in routes[0].label
|
||||
|
||||
|
||||
def test_list_local_routes_from_nested_route_directory(tmp_path):
|
||||
route_name = "bbbbbbbbbbbbbbbb|2026-07-03--13-45-00"
|
||||
_touch(tmp_path / route_name / "0" / "fcamera.hevc")
|
||||
|
||||
routes = list_local_routes(tmp_path)
|
||||
|
||||
assert len(routes) == 1
|
||||
assert routes[0].name == route_name
|
||||
assert routes[0].subtitle == "1 segment - road camera"
|
||||
|
||||
|
||||
def test_list_local_routes_ignores_invalid_entries(tmp_path):
|
||||
_touch(tmp_path / "not-a-route" / "fcamera.hevc")
|
||||
|
||||
assert list_local_routes(tmp_path) == []
|
||||
31
iqpilot/selfdrive/ui/tests/test_motd.py
Normal file
31
iqpilot/selfdrive/ui/tests/test_motd.py
Normal file
@@ -0,0 +1,31 @@
|
||||
from types import SimpleNamespace
|
||||
|
||||
from iqpilot.selfdrive.ui.lib import motd
|
||||
|
||||
|
||||
def test_load_motds_uses_verified_module_and_dongle_override(monkeypatch):
|
||||
calls = []
|
||||
|
||||
def import_verified_module(bundle, module):
|
||||
calls.append((bundle, module))
|
||||
return SimpleNamespace(messages_for_dongle=lambda dongle_id: (" Staff fleet ", "", 42))
|
||||
|
||||
monkeypatch.setattr(
|
||||
"iqpilot.system.proprietary_runtime._verified_import.import_verified_module",
|
||||
import_verified_module,
|
||||
)
|
||||
|
||||
assert motd.load_motds("0123456789ABCDEF") == ["Staff fleet"]
|
||||
assert calls == [(motd._BUNDLE_NAME, motd._MODULE_NAME)]
|
||||
|
||||
|
||||
def test_load_motds_falls_back_when_verified_import_is_unavailable(monkeypatch):
|
||||
def fail_import(*_args):
|
||||
raise ImportError("bundle unavailable")
|
||||
|
||||
monkeypatch.setattr(
|
||||
"iqpilot.system.proprietary_runtime._verified_import.import_verified_module",
|
||||
fail_import,
|
||||
)
|
||||
|
||||
assert motd.load_motds("0123456789abcdef") == list(motd.FALLBACK_MOTDS)
|
||||
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-day-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-day-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
|
||||
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
|
||||
8
iqpilot/selfdrive/ui/tests/test_raylib_ui.py
Normal file
8
iqpilot/selfdrive/ui/tests/test_raylib_ui.py
Normal file
@@ -0,0 +1,8 @@
|
||||
import time
|
||||
from iqpilot.selfdrive.test.helpers import with_processes
|
||||
|
||||
|
||||
@with_processes(["ui"])
|
||||
def test_raylib_ui():
|
||||
"""Test initialization of the UI widgets is successful."""
|
||||
time.sleep(1)
|
||||
35
iqpilot/selfdrive/ui/tests/test_soundd.py
Normal file
35
iqpilot/selfdrive/ui/tests/test_soundd.py
Normal file
@@ -0,0 +1,35 @@
|
||||
from iqpilot.cereal import car
|
||||
from iqpilot.cereal import messaging
|
||||
from iqpilot.cereal.messaging import SubMaster, PubMaster
|
||||
from iqpilot.selfdrive.ui.soundd import SELFDRIVE_STATE_TIMEOUT, check_selfdrive_timeout_alert
|
||||
|
||||
import time
|
||||
|
||||
AudibleAlert = car.CarControl.HUDControl.AudibleAlert
|
||||
|
||||
|
||||
class TestSoundd:
|
||||
def test_check_selfdrive_timeout_alert(self):
|
||||
sm = SubMaster(['selfdriveState'])
|
||||
pm = PubMaster(['selfdriveState'])
|
||||
|
||||
for _ in range(100):
|
||||
cs = messaging.new_message('selfdriveState')
|
||||
cs.selfdriveState.enabled = True
|
||||
|
||||
pm.send("selfdriveState", cs)
|
||||
|
||||
time.sleep(0.01)
|
||||
|
||||
sm.update(0)
|
||||
|
||||
assert not check_selfdrive_timeout_alert(sm)
|
||||
|
||||
for _ in range(SELFDRIVE_STATE_TIMEOUT * 110):
|
||||
sm.update(0)
|
||||
time.sleep(0.01)
|
||||
|
||||
assert check_selfdrive_timeout_alert(sm)
|
||||
|
||||
# TODO: add test with micd for checking that soundd actually outputs sounds
|
||||
|
||||
124
iqpilot/selfdrive/ui/tests/test_translations.py
Normal file
124
iqpilot/selfdrive/ui/tests/test_translations.py
Normal file
@@ -0,0 +1,124 @@
|
||||
import pytest
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import xml.etree.ElementTree as ET
|
||||
import string
|
||||
import requests
|
||||
from parameterized import parameterized_class
|
||||
from iqpilot.system.ui.lib.multilang import TRANSLATIONS_DIR, LANGUAGES_FILE
|
||||
|
||||
with open(str(LANGUAGES_FILE)) as f:
|
||||
translation_files = json.load(f)
|
||||
|
||||
UNFINISHED_TRANSLATION_TAG = "<translation type=\"unfinished\"" # non-empty translations can be marked unfinished
|
||||
LOCATION_TAG = "<location "
|
||||
FORMAT_ARG = re.compile("%[0-9]+")
|
||||
|
||||
|
||||
@pytest.mark.skip("TODO: update for raylib")
|
||||
@parameterized_class(("name", "file"), translation_files.items())
|
||||
class TestTranslations:
|
||||
name: str
|
||||
file: str
|
||||
|
||||
@staticmethod
|
||||
def _read_translation_file(path, file):
|
||||
tr_file = os.path.join(path, f"{file}.ts")
|
||||
with open(tr_file) as f:
|
||||
return f.read()
|
||||
|
||||
def test_missing_translation_files(self):
|
||||
assert os.path.exists(os.path.join(str(TRANSLATIONS_DIR), f"{self.file}.ts")), \
|
||||
f"{self.name} has no XML translation file, run scripts/iqpilot/translations/update_translations.py"
|
||||
|
||||
@pytest.mark.skip("Only test unfinished translations before going to release")
|
||||
def test_unfinished_translations(self):
|
||||
cur_translations = self._read_translation_file(TRANSLATIONS_DIR, self.file)
|
||||
assert UNFINISHED_TRANSLATION_TAG not in cur_translations, \
|
||||
f"{self.file} ({self.name}) translation file has unfinished translations. Finish translations or mark them as completed in Qt Linguist"
|
||||
|
||||
def test_vanished_translations(self):
|
||||
cur_translations = self._read_translation_file(TRANSLATIONS_DIR, self.file)
|
||||
assert "<translation type=\"vanished\">" not in cur_translations, \
|
||||
f"{self.file} ({self.name}) translation file has obsolete translations. Run scripts/iqpilot/translations/update_translations.py"
|
||||
|
||||
def test_finished_translations(self):
|
||||
"""
|
||||
Tests ran on each translation marked "finished"
|
||||
Plural:
|
||||
- that any numerus (plural) translations have all plural forms non-empty
|
||||
- that the correct format specifier is used (%n)
|
||||
Non-plural:
|
||||
- that translation is not empty
|
||||
- that translation format arguments are consistent
|
||||
"""
|
||||
tr_xml = ET.parse(os.path.join(TRANSLATIONS_DIR, f"{self.file}.ts"))
|
||||
|
||||
for context in tr_xml.getroot():
|
||||
for message in context.iterfind("message"):
|
||||
translation = message.find("translation")
|
||||
source_text = message.find("source").text
|
||||
|
||||
# Do not test unfinished translations
|
||||
if translation.get("type") == "unfinished":
|
||||
continue
|
||||
|
||||
if message.get("numerus") == "yes":
|
||||
numerusform = [t.text for t in translation.findall("numerusform")]
|
||||
|
||||
for nf in numerusform:
|
||||
assert nf is not None, f"Ensure all plural translation forms are completed: {source_text}"
|
||||
assert "%n" in nf, "Ensure numerus argument (%n) exists in translation."
|
||||
assert FORMAT_ARG.search(nf) is None, f"Plural translations must use %n, not %1, %2, etc.: {numerusform}"
|
||||
|
||||
else:
|
||||
assert translation.text is not None, f"Ensure translation is completed: {source_text}"
|
||||
|
||||
source_args = FORMAT_ARG.findall(source_text)
|
||||
translation_args = FORMAT_ARG.findall(translation.text)
|
||||
assert sorted(source_args) == sorted(translation_args), \
|
||||
f"Ensure format arguments are consistent: `{source_text}` vs. `{translation.text}`"
|
||||
|
||||
def test_no_locations(self):
|
||||
for line in self._read_translation_file(TRANSLATIONS_DIR, self.file).splitlines():
|
||||
assert not line.strip().startswith(LOCATION_TAG), \
|
||||
f"Line contains location tag: {line.strip()}, remove all line numbers."
|
||||
|
||||
def test_entities_error(self):
|
||||
cur_translations = self._read_translation_file(TRANSLATIONS_DIR, self.file)
|
||||
matches = re.findall(r'@(\w+);', cur_translations)
|
||||
assert len(matches) == 0, f"The string(s) {matches} were found with '@' instead of '&'"
|
||||
|
||||
def test_bad_language(self):
|
||||
IGNORED_WORDS = {'pédale'}
|
||||
|
||||
match = re.search(r'([a-zA-Z]{2,3})', self.file)
|
||||
assert match, f"{self.name} - could not parse language"
|
||||
|
||||
try:
|
||||
response = requests.get(
|
||||
f"https://raw.githubusercontent.com/LDNOOBW/List-of-Dirty-Naughty-Obscene-and-Otherwise-Bad-Words/master/{match.group(1)}"
|
||||
)
|
||||
response.raise_for_status()
|
||||
except requests.exceptions.HTTPError as e:
|
||||
if e.response is not None and e.response.status_code == 429:
|
||||
pytest.skip("word list rate limited")
|
||||
raise
|
||||
|
||||
banned_words = {line.strip() for line in response.text.splitlines()}
|
||||
|
||||
for context in ET.parse(os.path.join(TRANSLATIONS_DIR, f"{self.file}.ts")).getroot():
|
||||
for message in context.iterfind("message"):
|
||||
translation = message.find("translation")
|
||||
if translation.get("type") == "unfinished":
|
||||
continue
|
||||
|
||||
translation_text = " ".join([t.text for t in translation.findall("numerusform")]) if message.get("numerus") == "yes" else translation.text
|
||||
|
||||
if not translation_text:
|
||||
continue
|
||||
|
||||
words = set(translation_text.translate(str.maketrans('', '', string.punctuation + '%n')).lower().split())
|
||||
bad_words_found = words & (banned_words - IGNORED_WORDS)
|
||||
assert not bad_words_found, f"Bad language found in {self.name}: '{translation_text}'. Bad word(s): {', '.join(bad_words_found)}"
|
||||
Reference in New Issue
Block a user