IQ.Pilot Prebuilt Release @ 27f668a
This commit is contained in:
60
iqpilot/selfdrive/selfdrived/tests/test_alertmanager.py
Normal file
60
iqpilot/selfdrive/selfdrived/tests/test_alertmanager.py
Normal file
@@ -0,0 +1,60 @@
|
||||
import random
|
||||
|
||||
from iqpilot.selfdrive.selfdrived.events import Alert, EVENTS
|
||||
from iqpilot.selfdrive.selfdrived.alertmanager import AlertManager
|
||||
|
||||
from iqpilot.common.atlas_alerts import NULL_ALERT as EmptyAlert
|
||||
|
||||
|
||||
class TestAlertManager:
|
||||
|
||||
def test_duration(self):
|
||||
"""
|
||||
Enforce that an alert lasts for max(alert duration, duration the alert is added)
|
||||
"""
|
||||
for duration in range(1, 100):
|
||||
alert = None
|
||||
while not isinstance(alert, Alert):
|
||||
event = random.choice([e for e in EVENTS.values() if len(e)])
|
||||
alert = random.choice(list(event.values()))
|
||||
|
||||
alert.duration = duration
|
||||
|
||||
# check two cases:
|
||||
# - alert is added to AM for <= the alert's duration
|
||||
# - alert is added to AM for > alert's duration
|
||||
for greater in (True, False):
|
||||
if greater:
|
||||
add_duration = duration + random.randint(1, 10)
|
||||
else:
|
||||
add_duration = random.randint(1, duration)
|
||||
show_duration = max(duration, add_duration)
|
||||
|
||||
AM = AlertManager()
|
||||
for frame in range(duration+10):
|
||||
if frame < add_duration:
|
||||
AM.add_many(frame, [alert, ])
|
||||
AM.process_alerts(frame, set())
|
||||
|
||||
shown = AM.current_alert != EmptyAlert
|
||||
should_show = frame <= show_duration
|
||||
assert shown == should_show, f"{frame=} {add_duration=} {duration=}"
|
||||
|
||||
# check one case:
|
||||
# - if alert is re-added to AM before it ends the duration is extended
|
||||
if duration > 1:
|
||||
AM = AlertManager()
|
||||
show_duration = duration * 2
|
||||
for frame in range(duration * 2 + 10):
|
||||
if frame == 0:
|
||||
AM.add_many(frame, [alert, ])
|
||||
|
||||
if frame == duration:
|
||||
# add alert one frame before it ends
|
||||
assert AM.current_alert == alert
|
||||
AM.add_many(frame, [alert, ])
|
||||
AM.process_alerts(frame, set())
|
||||
|
||||
shown = AM.current_alert != EmptyAlert
|
||||
should_show = frame <= show_duration
|
||||
assert shown == should_show, f"{frame=} {duration=}"
|
||||
163
iqpilot/selfdrive/selfdrived/tests/test_alerts.py
Normal file
163
iqpilot/selfdrive/selfdrived/tests/test_alerts.py
Normal file
@@ -0,0 +1,163 @@
|
||||
import copy
|
||||
import json
|
||||
import os
|
||||
import random
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
|
||||
from iqpilot.cereal import log, car, custom
|
||||
from iqpilot.cereal.messaging import SubMaster
|
||||
from iqpilot.common.basedir import BASEDIR
|
||||
from iqpilot.common.params import Params
|
||||
from iqpilot.selfdrive.selfdrived.events import Alert, EVENTS, ET
|
||||
from iqpilot.selfdrive.selfdrived.iq_events import EVENTS_IQ
|
||||
from iqpilot.selfdrive.selfdrived.events import invalid_lkas_setting_alert, invalid_lkas_setting_no_entry_alert
|
||||
from iqpilot.selfdrive.selfdrived.alertmanager import set_offroad_alert
|
||||
from iqpilot.selfdrive.test.process_replay.process_replay import CONFIGS
|
||||
|
||||
AlertSize = log.SelfdriveState.AlertSize
|
||||
|
||||
OFFROAD_ALERTS_PATH = os.path.join(BASEDIR, "iqpilot/selfdrive/selfdrived/alerts_offroad.json")
|
||||
|
||||
# TODO: add callback alerts
|
||||
ALERTS = []
|
||||
for event_types in EVENTS.values():
|
||||
for alert in event_types.values():
|
||||
ALERTS.append(alert)
|
||||
|
||||
|
||||
class TestAlerts:
|
||||
|
||||
def test_wrong_gear_alerts_are_silent_and_invisible(self):
|
||||
wrong_gear_alerts = (EVENTS[log.OnroadEvent.EventName.wrongGear][ET.SOFT_DISABLE],
|
||||
EVENTS[log.OnroadEvent.EventName.wrongGear][ET.NO_ENTRY],
|
||||
EVENTS_IQ[custom.IQOnroadEvent.EventName.gearNotDriveSilent][ET.NO_ENTRY])
|
||||
|
||||
for alert in wrong_gear_alerts:
|
||||
assert alert.alert_size == AlertSize.none
|
||||
assert alert.audible_alert == car.CarControl.HUDControl.AudibleAlert.none
|
||||
assert alert.alert_text_1 == ""
|
||||
assert alert.alert_text_2 == ""
|
||||
|
||||
@classmethod
|
||||
def setup_class(cls):
|
||||
with open(OFFROAD_ALERTS_PATH) as f:
|
||||
cls.offroad_alerts = json.loads(f.read())
|
||||
|
||||
# Create fake objects for callback
|
||||
cls.CS = car.CarState.new_message()
|
||||
cls.CP = car.CarParams.new_message()
|
||||
cfg = [c for c in CONFIGS if c.proc_name == 'selfdrived'][0]
|
||||
cls.sm = SubMaster(cfg.pubs)
|
||||
|
||||
def test_events_defined(self):
|
||||
# Ensure all events in capnp schema are defined in events.py
|
||||
events = log.OnroadEvent.EventName.schema.enumerants
|
||||
|
||||
for name, e in events.items():
|
||||
if not name.endswith("DEPRECATED") and not name.startswith("eventReserved"):
|
||||
fail_msg = f"{name} @{e} not in EVENTS"
|
||||
assert e in EVENTS.keys(), fail_msg
|
||||
|
||||
# ensure alert text doesn't exceed allowed width
|
||||
def test_alert_text_length(self):
|
||||
font_path = os.path.join(BASEDIR, "iqpilot/selfdrive/assets/fonts")
|
||||
regular_font_path = os.path.join(font_path, "Inter-SemiBold.ttf")
|
||||
bold_font_path = os.path.join(font_path, "Inter-Bold.ttf")
|
||||
semibold_font_path = os.path.join(font_path, "Inter-SemiBold.ttf")
|
||||
|
||||
max_text_width = 2160 - 300 # full screen width is usable, minus sidebar
|
||||
draw = ImageDraw.Draw(Image.new('RGB', (0, 0)))
|
||||
|
||||
fonts = {
|
||||
AlertSize.small: [ImageFont.truetype(semibold_font_path, 74)],
|
||||
AlertSize.mid: [ImageFont.truetype(bold_font_path, 88),
|
||||
ImageFont.truetype(regular_font_path, 66)],
|
||||
}
|
||||
|
||||
for alert in ALERTS:
|
||||
if not isinstance(alert, Alert):
|
||||
alert = alert(self.CP, self.CS, self.sm, metric=False, soft_disable_time=100, personality=log.LongitudinalPersonality.standard)
|
||||
|
||||
# for full size alerts, both text fields wrap the text,
|
||||
# so it's unlikely that they would go past the max width
|
||||
if alert.alert_size in (AlertSize.none, AlertSize.full):
|
||||
continue
|
||||
|
||||
for i, txt in enumerate([alert.alert_text_1, alert.alert_text_2]):
|
||||
if i >= len(fonts[alert.alert_size]):
|
||||
break
|
||||
|
||||
font = fonts[alert.alert_size][i]
|
||||
left, _, right, _ = draw.textbbox((0, 0), txt, font)
|
||||
width = right - left
|
||||
msg = f"type: {alert.alert_type} msg: {txt}"
|
||||
assert width <= max_text_width, msg
|
||||
|
||||
def test_alert_sanity_check(self):
|
||||
for event_types in EVENTS.values():
|
||||
for event_type, a in event_types.items():
|
||||
# TODO: add callback alerts
|
||||
if not isinstance(a, Alert):
|
||||
continue
|
||||
|
||||
if a.alert_size == AlertSize.none:
|
||||
assert len(a.alert_text_1) == 0
|
||||
assert len(a.alert_text_2) == 0
|
||||
elif a.alert_size == AlertSize.small:
|
||||
assert len(a.alert_text_1) > 0
|
||||
assert len(a.alert_text_2) == 0
|
||||
elif a.alert_size == AlertSize.mid:
|
||||
assert len(a.alert_text_1) > 0
|
||||
assert len(a.alert_text_2) > 0
|
||||
else:
|
||||
assert len(a.alert_text_1) > 0
|
||||
|
||||
assert a.duration >= 0.
|
||||
|
||||
if event_type not in (ET.WARNING, ET.PERMANENT, ET.PRE_ENABLE):
|
||||
assert a.creation_delay == 0.
|
||||
|
||||
def test_offroad_alerts(self):
|
||||
params = Params()
|
||||
for a in self.offroad_alerts:
|
||||
# set the alert
|
||||
alert = copy.copy(self.offroad_alerts[a])
|
||||
set_offroad_alert(a, True)
|
||||
alert['extra'] = ''
|
||||
assert alert == params.get(a)
|
||||
|
||||
# then delete it
|
||||
set_offroad_alert(a, False)
|
||||
assert params.get(a) is None
|
||||
|
||||
def test_offroad_alerts_extra_text(self):
|
||||
params = Params()
|
||||
for i in range(50):
|
||||
# set the alert
|
||||
a = random.choice(list(self.offroad_alerts))
|
||||
alert = self.offroad_alerts[a]
|
||||
set_offroad_alert(a, True, extra_text="a"*i)
|
||||
|
||||
written_alert = params.get(a)
|
||||
assert "a"*i == written_alert['extra']
|
||||
assert alert["text"] == written_alert['text']
|
||||
|
||||
def test_invalid_lkas_setting_alert_tesla_dashcam_mode(self):
|
||||
self.CP.brand = "tesla"
|
||||
alert = invalid_lkas_setting_alert(self.CP, self.CS, self.sm, metric=False, soft_disable_time=100, personality=log.LongitudinalPersonality.standard)
|
||||
no_entry = invalid_lkas_setting_no_entry_alert(self.CP, self.CS, self.sm, metric=False, soft_disable_time=100, personality=log.LongitudinalPersonality.standard)
|
||||
|
||||
assert alert.alert_text_1 == "Dashcam Mode"
|
||||
assert alert.alert_text_2 == "FSD / Autosteer is active"
|
||||
assert no_entry.alert_text_1 == "Dashcam Mode"
|
||||
assert no_entry.alert_text_2 == "FSD / Autosteer is active"
|
||||
|
||||
def test_invalid_lkas_setting_alert_non_tesla_unchanged(self):
|
||||
self.CP.brand = "mazda"
|
||||
alert = invalid_lkas_setting_alert(self.CP, self.CS, self.sm, metric=False, soft_disable_time=100, personality=log.LongitudinalPersonality.standard)
|
||||
no_entry = invalid_lkas_setting_no_entry_alert(self.CP, self.CS, self.sm, metric=False, soft_disable_time=100, personality=log.LongitudinalPersonality.standard)
|
||||
|
||||
assert alert.alert_text_1 == "Invalid LKAS setting"
|
||||
assert alert.alert_text_2 == "Enable your car's LKAS to engage"
|
||||
assert no_entry.alert_text_1 == "IQ.Pilot Unavailable"
|
||||
assert no_entry.alert_text_2 == "Invalid LKAS setting"
|
||||
79
iqpilot/selfdrive/selfdrived/tests/test_iq_camera_alerts.py
Normal file
79
iqpilot/selfdrive/selfdrived/tests/test_iq_camera_alerts.py
Normal file
@@ -0,0 +1,79 @@
|
||||
import copy
|
||||
from types import SimpleNamespace
|
||||
|
||||
from iqpilot.cereal import car, custom, log
|
||||
from iqpilot.common.atlas_alerts import HardDisableCard, Tags as ET, Tier as Priority
|
||||
from iqpilot.selfdrive.selfdrived.alertmanager import AlertManager
|
||||
from iqpilot.selfdrive.selfdrived.events import EVENTS
|
||||
from iqpilot.selfdrive.selfdrived import iq_events
|
||||
|
||||
|
||||
def alert(camera_type, *, report_id="", chime=False, distance=300.0):
|
||||
nav = SimpleNamespace(
|
||||
cameraType=camera_type,
|
||||
cameraDistance=distance,
|
||||
cameraSpeedLimit=25.0,
|
||||
cameraAlertId=report_id,
|
||||
cameraChime=chime,
|
||||
)
|
||||
return iq_events.speed_camera_alert(None, None, {"iqNavState": nav}, False, 0, None)
|
||||
|
||||
|
||||
def test_existing_camera_audio_is_unchanged():
|
||||
result = alert(custom.IQNavState.CameraType.fixedSpeed)
|
||||
assert result.audible_alert == car.CarControl.HUDControl.AudibleAlert.prompt
|
||||
|
||||
|
||||
def test_police_visual_mode_is_silent():
|
||||
result = alert(custom.IQNavState.CameraType.police, report_id="visual", chime=False)
|
||||
assert result.audible_alert == car.CarControl.HUDControl.AudibleAlert.none
|
||||
|
||||
|
||||
def test_police_chime_is_deduplicated_by_report():
|
||||
iq_events._POLICE_CHIMED_IDS.clear()
|
||||
first = alert(custom.IQNavState.CameraType.police, report_id="police-a", chime=True)
|
||||
second = alert(custom.IQNavState.CameraType.police, report_id="police-a", chime=True)
|
||||
assert first.audible_alert == car.CarControl.HUDControl.AudibleAlert.prompt
|
||||
assert second.audible_alert == car.CarControl.HUDControl.AudibleAlert.none
|
||||
|
||||
|
||||
def test_alpr_wording_uses_configured_region(monkeypatch):
|
||||
monkeypatch.setattr(iq_events, "_configured_country_code", lambda: "US")
|
||||
assert alert(custom.IQNavState.CameraType.alpr).alert_text_1.startswith("Flock / ALPR Camera")
|
||||
assert alert(custom.IQNavState.CameraType.alpr, distance=0.0).alert_text_1 == "Flock Camera Detected"
|
||||
|
||||
monkeypatch.setattr(iq_events, "_configured_country_code", lambda: "DE")
|
||||
assert alert(custom.IQNavState.CameraType.alpr).alert_text_1.startswith("Traffic / ALPR Camera")
|
||||
assert alert(custom.IQNavState.CameraType.alpr, distance=0.0).alert_text_1 == "Traffic / ALPR Camera Detected"
|
||||
|
||||
|
||||
def test_missing_region_is_safe_and_preserves_flock_wording(monkeypatch):
|
||||
class UnavailableParams:
|
||||
def get(self, key):
|
||||
raise OSError(key)
|
||||
|
||||
monkeypatch.setattr(iq_events, "Params", UnavailableParams)
|
||||
assert iq_events._configured_country_code() == ""
|
||||
assert alert(custom.IQNavState.CameraType.alpr, distance=0.0).alert_text_1 == "Flock Camera Detected"
|
||||
|
||||
|
||||
def test_driver_attention_and_takeover_alerts_preempt_alpr():
|
||||
flock = alert(custom.IQNavState.CameraType.alpr, distance=0.0)
|
||||
pre_attention = copy.copy(EVENTS[log.OnroadEvent.EventName.preDriverDistracted][ET.PERMANENT])
|
||||
prompt_attention = copy.copy(EVENTS[log.OnroadEvent.EventName.promptDriverDistracted][ET.PERMANENT])
|
||||
takeover = copy.copy(EVENTS[log.OnroadEvent.EventName.driverDistracted][ET.PERMANENT])
|
||||
immediate_disable = HardDisableCard("Regression Test")
|
||||
|
||||
assert flock.priority == Priority.LOW
|
||||
assert pre_attention.priority == flock.priority + 1
|
||||
assert prompt_attention.priority == flock.priority + 1
|
||||
assert takeover.priority > flock.priority
|
||||
assert immediate_disable.priority > flock.priority
|
||||
|
||||
for expected in (pre_attention, prompt_attention, takeover, immediate_disable):
|
||||
manager = AlertManager()
|
||||
flock.alert_type = "flock/warning"
|
||||
expected.alert_type = f"expected/{expected.alert_text_1}"
|
||||
manager.add_many(0, [flock, expected])
|
||||
manager.process_alerts(0, set())
|
||||
assert manager.current_alert is expected
|
||||
@@ -0,0 +1,41 @@
|
||||
from iqpilot.cereal import car
|
||||
|
||||
from iqpilot.selfdrive.longitudinal_settings import get_valid_personality
|
||||
from iqpilot.selfdrive.selfdrived.selfdrived import _cleanup_startup_params
|
||||
|
||||
|
||||
class DummyParams:
|
||||
def __init__(self):
|
||||
self.removed: list[str] = []
|
||||
|
||||
def remove(self, key: str) -> None:
|
||||
self.removed.append(key)
|
||||
|
||||
|
||||
class TestLongitudinalPrefPersistence:
|
||||
def test_startup_cleanup_preserves_persistent_longitudinal_preferences(self):
|
||||
params = DummyParams()
|
||||
cp = car.CarParams()
|
||||
cp.alphaLongitudinalAvailable = False
|
||||
cp.openpilotLongitudinalControl = False
|
||||
|
||||
_cleanup_startup_params(cp, params)
|
||||
|
||||
assert params.removed == []
|
||||
|
||||
def test_invalid_personality_is_clamped_before_use(self):
|
||||
class ParamsWithInvalidPersonality:
|
||||
def __init__(self):
|
||||
self.value = 3
|
||||
|
||||
def get(self, key: str, return_default: bool = False) -> int:
|
||||
assert key == "LongitudinalPersonality"
|
||||
return self.value
|
||||
|
||||
def put(self, key: str, value: int) -> None:
|
||||
assert key == "LongitudinalPersonality"
|
||||
self.value = value
|
||||
|
||||
params = ParamsWithInvalidPersonality()
|
||||
assert get_valid_personality(params) == 2
|
||||
assert params.value == 2
|
||||
141
iqpilot/selfdrive/selfdrived/tests/test_longitudinal_settings.py
Normal file
141
iqpilot/selfdrive/selfdrived/tests/test_longitudinal_settings.py
Normal file
@@ -0,0 +1,141 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from iqpilot.selfdrive.longitudinal_settings import (
|
||||
LONGITUDINAL_MODE_CHILL,
|
||||
LONGITUDINAL_MODE_DYNAMIC,
|
||||
LONGITUDINAL_MODE_PILOT,
|
||||
LONGITUDINAL_MODE_STOCK,
|
||||
PERSONALITY_AGGRESSIVE,
|
||||
PERSONALITY_RELAXED,
|
||||
PERSONALITY_STANDARD,
|
||||
PERSONALITY_VALUES,
|
||||
apply_longitudinal_mode,
|
||||
get_follow_distance_state,
|
||||
get_longitudinal_mode,
|
||||
get_runtime_personality,
|
||||
longitudinal_mode_needs_cycle,
|
||||
next_longitudinal_mode,
|
||||
set_valid_personality,
|
||||
)
|
||||
|
||||
|
||||
class Params:
|
||||
def __init__(self, personality=PERSONALITY_STANDARD):
|
||||
self.values = {
|
||||
"AlphaLongitudinalEnabled": True,
|
||||
"ExperimentalMode": True,
|
||||
"IQDynamicMode": True,
|
||||
"LongitudinalPersonality": personality,
|
||||
}
|
||||
self.personality_writes = []
|
||||
|
||||
def get(self, key, return_default=False):
|
||||
return self.values[key]
|
||||
|
||||
def get_bool(self, key):
|
||||
return bool(self.values[key])
|
||||
|
||||
def put(self, key, value):
|
||||
self.values[key] = value
|
||||
if key == "LongitudinalPersonality":
|
||||
self.personality_writes.append(value)
|
||||
|
||||
def put_bool(self, key, value):
|
||||
self.values[key] = bool(value)
|
||||
|
||||
|
||||
def test_personality_writer_rejects_stock_value():
|
||||
params = Params()
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
set_valid_personality(params, 3)
|
||||
|
||||
assert params.personality_writes == []
|
||||
|
||||
|
||||
def test_mode_paths_only_write_valid_personalities():
|
||||
params = Params(PERSONALITY_AGGRESSIVE)
|
||||
|
||||
for mode in range(4):
|
||||
apply_longitudinal_mode(params, mode)
|
||||
|
||||
assert all(value in PERSONALITY_VALUES for value in params.personality_writes)
|
||||
|
||||
|
||||
def test_stock_mode_preserves_personality_and_dynamic_restores_it():
|
||||
params = Params(PERSONALITY_AGGRESSIVE)
|
||||
|
||||
apply_longitudinal_mode(params, LONGITUDINAL_MODE_STOCK)
|
||||
|
||||
assert get_follow_distance_state(params) == (None, False)
|
||||
assert get_runtime_personality(params) == PERSONALITY_AGGRESSIVE
|
||||
assert params.values["LongitudinalPersonality"] == PERSONALITY_AGGRESSIVE
|
||||
assert params.personality_writes == []
|
||||
|
||||
apply_longitudinal_mode(params, LONGITUDINAL_MODE_DYNAMIC)
|
||||
|
||||
assert get_longitudinal_mode(params) == LONGITUDINAL_MODE_DYNAMIC
|
||||
assert get_follow_distance_state(params) == (PERSONALITY_AGGRESSIVE, True)
|
||||
|
||||
|
||||
def test_stock_mode_sanitizes_legacy_stock_personality_value():
|
||||
params = Params(3)
|
||||
|
||||
apply_longitudinal_mode(params, LONGITUDINAL_MODE_STOCK)
|
||||
|
||||
assert get_follow_distance_state(params) == (None, False)
|
||||
assert params.values["LongitudinalPersonality"] == PERSONALITY_RELAXED
|
||||
assert params.personality_writes == [PERSONALITY_RELAXED]
|
||||
|
||||
|
||||
def test_chill_mode_forces_relaxed_personality():
|
||||
params = Params(PERSONALITY_AGGRESSIVE)
|
||||
|
||||
apply_longitudinal_mode(params, LONGITUDINAL_MODE_CHILL)
|
||||
|
||||
assert get_follow_distance_state(params) == (PERSONALITY_RELAXED, False)
|
||||
assert get_runtime_personality(params) == PERSONALITY_RELAXED
|
||||
assert params.values["LongitudinalPersonality"] == PERSONALITY_RELAXED
|
||||
assert params.personality_writes == [PERSONALITY_RELAXED]
|
||||
|
||||
|
||||
def test_dynamic_and_pilot_enable_valid_personality_selection():
|
||||
params = Params(PERSONALITY_STANDARD)
|
||||
|
||||
assert get_follow_distance_state(params) == (PERSONALITY_STANDARD, True)
|
||||
|
||||
params.values["IQDynamicMode"] = False
|
||||
|
||||
assert get_follow_distance_state(params) == (PERSONALITY_STANDARD, True)
|
||||
|
||||
|
||||
def test_onroad_cycles_only_between_iq_modes():
|
||||
assert next_longitudinal_mode(LONGITUDINAL_MODE_CHILL, True, True) == LONGITUDINAL_MODE_DYNAMIC
|
||||
assert next_longitudinal_mode(LONGITUDINAL_MODE_DYNAMIC, True, True) == LONGITUDINAL_MODE_PILOT
|
||||
assert next_longitudinal_mode(LONGITUDINAL_MODE_PILOT, True, True) == LONGITUDINAL_MODE_CHILL
|
||||
|
||||
|
||||
def test_onroad_stock_acc_is_locked():
|
||||
assert next_longitudinal_mode(LONGITUDINAL_MODE_STOCK, True, True) == LONGITUDINAL_MODE_STOCK
|
||||
assert next_longitudinal_mode(LONGITUDINAL_MODE_STOCK, True, False) == LONGITUDINAL_MODE_STOCK
|
||||
|
||||
|
||||
def test_offroad_cycles_through_stock_acc():
|
||||
assert next_longitudinal_mode(LONGITUDINAL_MODE_PILOT, False, True) == LONGITUDINAL_MODE_STOCK
|
||||
assert next_longitudinal_mode(LONGITUDINAL_MODE_STOCK, False, True) == LONGITUDINAL_MODE_CHILL
|
||||
|
||||
|
||||
def test_offroad_without_iq_modes_only_offers_stock_acc():
|
||||
assert next_longitudinal_mode(LONGITUDINAL_MODE_STOCK, False, False) == LONGITUDINAL_MODE_STOCK
|
||||
assert next_longitudinal_mode(LONGITUDINAL_MODE_PILOT, False, False) == LONGITUDINAL_MODE_STOCK
|
||||
|
||||
|
||||
def test_cycle_only_when_crossing_stock_boundary():
|
||||
assert longitudinal_mode_needs_cycle(LONGITUDINAL_MODE_STOCK, LONGITUDINAL_MODE_CHILL)
|
||||
assert longitudinal_mode_needs_cycle(LONGITUDINAL_MODE_PILOT, LONGITUDINAL_MODE_STOCK)
|
||||
assert not longitudinal_mode_needs_cycle(LONGITUDINAL_MODE_CHILL, LONGITUDINAL_MODE_PILOT)
|
||||
assert not longitudinal_mode_needs_cycle(LONGITUDINAL_MODE_DYNAMIC, LONGITUDINAL_MODE_CHILL)
|
||||
92
iqpilot/selfdrive/selfdrived/tests/test_state_machine.py
Normal file
92
iqpilot/selfdrive/selfdrived/tests/test_state_machine.py
Normal file
@@ -0,0 +1,92 @@
|
||||
from iqpilot.cereal import log
|
||||
from iqpilot.common.realtime import DT_CTRL
|
||||
from iqpilot.selfdrive.selfdrived.state import StateMachine, SOFT_DISABLE_TIME
|
||||
from iqpilot.selfdrive.selfdrived.events import Events, ET, EVENTS, NormalPermanentAlert
|
||||
|
||||
State = log.SelfdriveState.OpenpilotState
|
||||
|
||||
# The event types that maintain the current state
|
||||
MAINTAIN_STATES = {State.enabled: (None,), State.disabled: (None,), State.softDisabling: (ET.SOFT_DISABLE,),
|
||||
State.preEnabled: (ET.PRE_ENABLE,), State.overriding: (ET.OVERRIDE_LATERAL, ET.OVERRIDE_LONGITUDINAL)}
|
||||
ALL_STATES = tuple(State.schema.enumerants.values())
|
||||
# The event types checked in DISABLED section of state machine
|
||||
ENABLE_EVENT_TYPES = (ET.ENABLE, ET.PRE_ENABLE, ET.OVERRIDE_LATERAL, ET.OVERRIDE_LONGITUDINAL)
|
||||
|
||||
|
||||
def make_event(event_types):
|
||||
event = {}
|
||||
for ev in event_types:
|
||||
event[ev] = NormalPermanentAlert("alert")
|
||||
EVENTS[0] = event
|
||||
return 0
|
||||
|
||||
|
||||
class TestStateMachine:
|
||||
def setup_method(self):
|
||||
self.events = Events()
|
||||
self.state_machine = StateMachine()
|
||||
self.state_machine.soft_disable_timer = int(SOFT_DISABLE_TIME / DT_CTRL)
|
||||
|
||||
def test_immediate_disable(self):
|
||||
for state in ALL_STATES:
|
||||
for et in MAINTAIN_STATES[state]:
|
||||
self.events.add(make_event([et, ET.IMMEDIATE_DISABLE]))
|
||||
self.state_machine.state = state
|
||||
self.state_machine.update(self.events)
|
||||
assert State.disabled == self.state_machine.state
|
||||
self.events.clear()
|
||||
|
||||
def test_user_disable(self):
|
||||
for state in ALL_STATES:
|
||||
for et in MAINTAIN_STATES[state]:
|
||||
self.events.add(make_event([et, ET.USER_DISABLE]))
|
||||
self.state_machine.state = state
|
||||
self.state_machine.update(self.events)
|
||||
assert State.disabled == self.state_machine.state
|
||||
self.events.clear()
|
||||
|
||||
def test_soft_disable(self):
|
||||
for state in ALL_STATES:
|
||||
if state == State.preEnabled: # preEnabled considers NO_ENTRY instead
|
||||
continue
|
||||
for et in MAINTAIN_STATES[state]:
|
||||
self.events.add(make_event([et, ET.SOFT_DISABLE]))
|
||||
self.state_machine.state = state
|
||||
self.state_machine.update(self.events)
|
||||
assert self.state_machine.state == State.disabled if state == State.disabled else State.softDisabling
|
||||
self.events.clear()
|
||||
|
||||
def test_soft_disable_timer(self):
|
||||
self.state_machine.state = State.enabled
|
||||
self.events.add(make_event([ET.SOFT_DISABLE]))
|
||||
self.state_machine.update(self.events)
|
||||
for _ in range(int(SOFT_DISABLE_TIME / DT_CTRL)):
|
||||
assert self.state_machine.state == State.softDisabling
|
||||
self.state_machine.update(self.events)
|
||||
|
||||
assert self.state_machine.state == State.disabled
|
||||
|
||||
def test_no_entry(self):
|
||||
# Make sure noEntry keeps us disabled
|
||||
for et in ENABLE_EVENT_TYPES:
|
||||
self.events.add(make_event([ET.NO_ENTRY, et]))
|
||||
self.state_machine.update(self.events)
|
||||
assert self.state_machine.state == State.disabled
|
||||
self.events.clear()
|
||||
|
||||
def test_no_entry_pre_enable(self):
|
||||
# preEnabled with noEntry event
|
||||
self.state_machine.state = State.preEnabled
|
||||
self.events.add(make_event([ET.NO_ENTRY, ET.PRE_ENABLE]))
|
||||
self.state_machine.update(self.events)
|
||||
assert self.state_machine.state == State.preEnabled
|
||||
|
||||
def test_maintain_states(self):
|
||||
# Given current state's event type, we should maintain state
|
||||
for state in ALL_STATES:
|
||||
for et in MAINTAIN_STATES[state]:
|
||||
self.state_machine.state = state
|
||||
self.events.add(make_event([et]))
|
||||
self.state_machine.update(self.events)
|
||||
assert self.state_machine.state == state
|
||||
self.events.clear()
|
||||
Reference in New Issue
Block a user