controlsd: add state machine tests (#24107)
* Handle NO_ENTRY in PRE_ENABLED
* add test
* add preEnabled NO_ENTRY test
* stash
* test soft disable
* tuples
* remove overriding until it's merged in
* use Events class
* fix tests and split out
* don't rely on controlsd's counter
old-commit-hash: d4f330447a
taco
parent
56bfed0c15
commit
8e11fbe2db
2 changed files with 128 additions and 16 deletions
@ -0,0 +1,105 @@ |
|||||||
|
#!/usr/bin/env python3 |
||||||
|
import unittest |
||||||
|
|
||||||
|
from cereal import car, log |
||||||
|
from common.realtime import DT_CTRL |
||||||
|
from selfdrive.car.car_helpers import interfaces |
||||||
|
from selfdrive.controls.controlsd import Controls, SOFT_DISABLE_TIME |
||||||
|
from selfdrive.controls.lib.events import Events, ET, Alert, Priority, AlertSize, AlertStatus, VisualAlert, \ |
||||||
|
AudibleAlert, EVENTS |
||||||
|
|
||||||
|
State = log.ControlsState.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} |
||||||
|
ALL_STATES = tuple((state for state in State.schema.enumerants.values() if |
||||||
|
state != State.overriding)) # TODO: remove overriding exception |
||||||
|
# The event types checked in DISABLED section of state machine |
||||||
|
ENABLE_EVENT_TYPES = (ET.ENABLE, ET.PRE_ENABLE) |
||||||
|
|
||||||
|
|
||||||
|
def make_event(event_types): |
||||||
|
event = {} |
||||||
|
for ev in event_types: |
||||||
|
event[ev] = Alert("", "", AlertStatus.normal, AlertSize.small, Priority.LOW, |
||||||
|
VisualAlert.none, AudibleAlert.none, 1.) |
||||||
|
EVENTS[0] = event |
||||||
|
return 0 |
||||||
|
|
||||||
|
|
||||||
|
class TestStateMachine(unittest.TestCase): |
||||||
|
|
||||||
|
def setUp(self): |
||||||
|
CarInterface, CarController, CarState = interfaces["mock"] |
||||||
|
CP = CarInterface.get_params("mock") |
||||||
|
CI = CarInterface(CP, CarController, CarState) |
||||||
|
|
||||||
|
self.controlsd = Controls(CI=CI) |
||||||
|
self.controlsd.events = Events() |
||||||
|
self.controlsd.soft_disable_timer = int(SOFT_DISABLE_TIME / DT_CTRL) |
||||||
|
self.CS = car.CarState() |
||||||
|
|
||||||
|
def test_immediate_disable(self): |
||||||
|
for state in ALL_STATES: |
||||||
|
self.controlsd.events.add(make_event([MAINTAIN_STATES[state], ET.IMMEDIATE_DISABLE])) |
||||||
|
self.controlsd.state = state |
||||||
|
self.controlsd.state_transition(self.CS) |
||||||
|
self.assertEqual(State.disabled, self.controlsd.state) |
||||||
|
self.controlsd.events.clear() |
||||||
|
|
||||||
|
def test_user_disable(self): |
||||||
|
for state in ALL_STATES: |
||||||
|
self.controlsd.events.add(make_event([MAINTAIN_STATES[state], ET.USER_DISABLE])) |
||||||
|
self.controlsd.state = state |
||||||
|
self.controlsd.state_transition(self.CS) |
||||||
|
self.assertEqual(State.disabled, self.controlsd.state) |
||||||
|
self.controlsd.events.clear() |
||||||
|
|
||||||
|
def test_soft_disable(self): |
||||||
|
for state in ALL_STATES: |
||||||
|
if state == State.preEnabled: # preEnabled considers NO_ENTRY instead |
||||||
|
continue |
||||||
|
self.controlsd.events.add(make_event([MAINTAIN_STATES[state], ET.SOFT_DISABLE])) |
||||||
|
self.controlsd.state = state |
||||||
|
self.controlsd.state_transition(self.CS) |
||||||
|
self.assertEqual(self.controlsd.state, State.disabled if state == State.disabled else State.softDisabling) |
||||||
|
self.controlsd.events.clear() |
||||||
|
|
||||||
|
def test_soft_disable_timer(self): |
||||||
|
self.controlsd.state = State.enabled |
||||||
|
self.controlsd.events.add(make_event([ET.SOFT_DISABLE])) |
||||||
|
self.controlsd.state_transition(self.CS) |
||||||
|
for _ in range(int(SOFT_DISABLE_TIME / DT_CTRL)): |
||||||
|
self.assertEqual(self.controlsd.state, State.softDisabling) |
||||||
|
self.controlsd.state_transition(self.CS) |
||||||
|
|
||||||
|
self.assertEqual(self.controlsd.state, State.disabled) |
||||||
|
|
||||||
|
def test_no_entry(self): |
||||||
|
# disabled with enable events |
||||||
|
for et in ENABLE_EVENT_TYPES: |
||||||
|
self.controlsd.events.add(make_event([ET.NO_ENTRY, et])) |
||||||
|
self.controlsd.state_transition(self.CS) |
||||||
|
self.assertEqual(self.controlsd.state, State.disabled) |
||||||
|
self.controlsd.events.clear() |
||||||
|
|
||||||
|
def test_no_entry_pre_enable(self): |
||||||
|
# preEnabled with preEnabled event |
||||||
|
self.controlsd.state = State.preEnabled |
||||||
|
self.controlsd.events.add(make_event([ET.NO_ENTRY, ET.PRE_ENABLE])) |
||||||
|
self.controlsd.state_transition(self.CS) |
||||||
|
self.assertEqual(self.controlsd.state, State.disabled) |
||||||
|
|
||||||
|
def test_maintain_states(self): |
||||||
|
# Given current state's event type, we should maintain state |
||||||
|
for state in ALL_STATES: |
||||||
|
self.controlsd.state = state |
||||||
|
self.controlsd.events.add(make_event([MAINTAIN_STATES[state]])) |
||||||
|
self.controlsd.state_transition(self.CS) |
||||||
|
self.assertEqual(self.controlsd.state, state) |
||||||
|
self.controlsd.events.clear() |
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__": |
||||||
|
unittest.main() |
Loading…
Reference in new issue