mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-08 13:33:42 +08:00
e52e1a6a86
Implement Sentry scanning, notification state and worker cancellation through explicit policies and preferences.
141 lines
6.1 KiB
Python
141 lines
6.1 KiB
Python
"""Frozen numeric motion behavior and fail-closed parked policy boundaries."""
|
|
|
|
from unittest.mock import Mock
|
|
|
|
import pytest
|
|
|
|
from openpilot.starpilot.sentry_mode.policy import (
|
|
Inputs, MotionSample, SentryPolicy, Settings,
|
|
)
|
|
|
|
|
|
def active(sample: MotionSample | None = None) -> Inputs:
|
|
return Inputs(enabled=True, offroad=True, voltage_ok=True, device_fresh=True, ignition_off=True, sample=sample)
|
|
|
|
|
|
def motion(now: float, z: float) -> MotionSample:
|
|
return MotionSample(now, (0.0, 0.0, z))
|
|
|
|
|
|
def fresh_through(policy: SentryPolicy, start: float, end: float, z: float = 9.8):
|
|
result = None
|
|
for tenth in range(round(start * 10) + 1, round(end * 10) + 1):
|
|
now = tenth / 10
|
|
result = policy.update(now, active(motion(now, z)))
|
|
return result
|
|
|
|
|
|
def armed(policy: SentryPolicy) -> None:
|
|
assert policy.update(0.0, active(motion(0.0, 9.8))).state == "arming"
|
|
assert fresh_through(policy, 0.0, 89.9).state == "arming"
|
|
assert policy.update(90.0, active(motion(90.0, 9.8))).state == "armed"
|
|
|
|
|
|
def test_frozen_arming_warning_and_alarm_numeric_sequence():
|
|
policy = SentryPolicy(Settings(sensitivity=0.1, warning_time_seconds=0.3))
|
|
armed(policy)
|
|
assert policy.settings.warning_trigger_count == 3
|
|
assert policy.update(90.1, active(motion(90.1, 10.0))).event is None
|
|
assert policy.update(90.2, active(motion(90.2, 9.8))).event is None
|
|
assert policy.update(90.3, active(motion(90.3, 10.0))).event == "warning"
|
|
assert policy.update(90.4, active(motion(90.4, 9.8))).event is None
|
|
# The frozen detector requires strictly more than 25 changes and 30 s from
|
|
# the first change. Feed 10 Hz fresh stable samples while that time elapses.
|
|
for index in range(5, 26):
|
|
now = 90.0 + index * 0.1
|
|
assert policy.update(now, active(motion(now, 10.0 if index % 2 else 9.8))).event is None
|
|
assert policy.trigger_count == 25
|
|
assert fresh_through(policy, 92.5, 120.0, 10.0).event is None
|
|
assert policy.update(120.1, active(motion(120.1, 9.8))).event == "alarm"
|
|
assert policy.update(120.2, active(motion(120.2, 10.0))).event is None
|
|
|
|
|
|
def test_duplicate_stale_invalid_and_out_of_order_samples_do_not_accumulate():
|
|
policy = SentryPolicy(Settings(sensitivity=0.1, warning_time_seconds=0.2))
|
|
armed(policy)
|
|
assert policy.update(90.1, active(motion(90.1, 10.0))).event is None
|
|
assert policy.update(90.2, active(motion(90.1, 9.8))).state == "sensor_unavailable"
|
|
assert policy.trigger_count == 0
|
|
assert policy.update(90.3, active(motion(90.1, 9.8))).state == "sensor_unavailable"
|
|
assert policy.update(90.4, active(motion(90.4, 9.8))).state == "arming"
|
|
assert policy.update(90.5, active(motion(90.3, 10.0))).state == "sensor_unavailable"
|
|
assert policy.update(90.6, active(motion(90.6, 9.8))).state == "arming"
|
|
|
|
|
|
def test_missing_sensor_during_arming_and_after_armed_requires_full_rearm():
|
|
policy = SentryPolicy()
|
|
assert policy.update(0.0, active()).state == "sensor_unavailable"
|
|
assert policy.update(0.1, active(motion(0.1, 9.8))).state == "arming"
|
|
assert fresh_through(policy, 0.1, 90.0).state == "arming"
|
|
assert policy.update(90.1, active()).state == "sensor_unavailable"
|
|
assert policy.update(90.2, active(motion(90.2, 9.8))).state == "arming"
|
|
assert fresh_through(policy, 90.2, 180.3).state == "armed"
|
|
assert policy.update(180.4, active()).state == "sensor_unavailable"
|
|
assert policy.update(180.5, active(motion(180.5, 9.8))).state == "arming"
|
|
|
|
|
|
def test_disabled_onroad_low_voltage_and_lost_device_rearm_for_full_90_seconds():
|
|
for stopped, expected in ((Inputs(False, True, True, True, True), "disabled"),
|
|
(Inputs(True, False, True, True, True), "disabled_onroad"),
|
|
(Inputs(True, True, False, True, True), "low_voltage"),
|
|
(Inputs(True, True, True, False, True), "unavailable"),
|
|
(Inputs(True, True, True, True, False), "disabled_ignition")):
|
|
policy = SentryPolicy()
|
|
armed(policy)
|
|
assert policy.update(90.1, active(motion(90.1, 10.0))).state == "armed"
|
|
assert policy.update(90.2, stopped).state == expected
|
|
assert policy.update(90.3, active(motion(90.3, 9.8))).state == "arming"
|
|
assert fresh_through(policy, 90.3, 180.2).state == "arming"
|
|
assert fresh_through(policy, 180.2, 180.4).state == "armed"
|
|
assert policy.trigger_count == 0
|
|
|
|
|
|
def test_monotonic_reversal_and_motion_window_expiry_fail_closed():
|
|
policy = SentryPolicy(Settings(sensitivity=0.1, warning_time_seconds=0.2))
|
|
armed(policy)
|
|
policy.update(90.1, active(motion(90.1, 10.0)))
|
|
assert policy.update(60.0, active(motion(60.0, 9.8))).state == "unavailable"
|
|
assert policy.update(60.1, active(motion(60.1, 9.8))).state == "arming"
|
|
assert fresh_through(policy, 60.1, 150.0).state == "arming"
|
|
assert fresh_through(policy, 150.0, 150.2).state == "armed"
|
|
assert policy.update(150.3, active(motion(150.3, 10.0))).event is None
|
|
assert fresh_through(policy, 150.3, 210.2, 10.0).event is None
|
|
assert policy.update(210.3, active(motion(210.3, 9.8))).event is None
|
|
assert policy.trigger_count == 1
|
|
|
|
|
|
def test_two_samples_90_seconds_apart_never_arm():
|
|
policy = SentryPolicy()
|
|
assert policy.update(0.0, active(motion(0.0, 9.8))).state == "arming"
|
|
assert policy.update(90.0, active(motion(90.0, 9.8))).state == "unavailable"
|
|
assert policy.update(90.1, active(motion(90.1, 9.8))).state == "arming"
|
|
assert fresh_through(policy, 90.1, 180.2).state == "armed"
|
|
|
|
|
|
@pytest.mark.parametrize("settings", [
|
|
{"sensitivity": 0.004}, {"sensitivity": float("nan")},
|
|
{"sensitivity": True}, {"sensitivity": "0.04"},
|
|
{"warning_time_seconds": 10.1}, {"warning_time_seconds": float("inf")},
|
|
{"warning_time_seconds": False},
|
|
])
|
|
def test_settings_reject_invalid_values(settings):
|
|
with pytest.raises(ValueError):
|
|
Settings(**settings)
|
|
|
|
|
|
@pytest.mark.parametrize("sample", [
|
|
(float("nan"), (0.0, 0.0, 9.8)),
|
|
(1.0, (0.0, float("inf"), 9.8)),
|
|
(1.0, (1e308, 0.0, 9.8)),
|
|
(1.0, (True, 0.0, 9.8)),
|
|
(1.0, (0.0, 9.8)),
|
|
])
|
|
def test_sample_requires_finite_time_and_three_axes(sample):
|
|
with pytest.raises(ValueError):
|
|
MotionSample(*sample)
|
|
|
|
|
|
def test_authority_inputs_reject_truthy_strings():
|
|
with pytest.raises(ValueError):
|
|
Mock(wraps=Inputs)("off", True, True, True, True)
|