Files
sunnypilot/openpilot/sunnypilot/sunnylink/tests/test_settings_changes.py
T

260 lines
9.7 KiB
Python

"""
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
Per-bug regression tests for the Raylib-vs-schema parity audit. Each test
isolates one of the gating bugs that the design-overhaul branch fixes so a
future regression is loud and obvious. These tests are intentionally narrow
and additive — they do not replace the broader test_settings_schema.py.
"""
from __future__ import annotations
import json
import os
from typing import Any
from openpilot.common.parameterized import parameterized
from openpilot.sunnypilot.sunnylink.tools.generate_settings_schema import (
DEFINITION_PATH,
TORQUE_VERSIONS_PATH,
_build_torque_options,
_load_torque_versions,
generate_schema,
)
from openpilot.sunnypilot.sunnylink.tools.validate_settings_ui import validate as validate_settings_ui
from openpilot.common.test import OpenpilotTestCase
def _walk_items(schema: dict[str, Any]):
"""Yield every item dict from the schema."""
def _yield(item: dict[str, Any]):
yield item
for sub in item.get("sub_items", []):
yield from _yield(sub)
for panel in schema.get("panels", []):
for section in panel.get("sections", []):
for item in section.get("items", []):
yield from _yield(item)
for sp in section.get("sub_panels", []):
for item in sp.get("items", []):
yield from _yield(item)
for item in panel.get("items", []):
yield from _yield(item)
for sp in panel.get("sub_panels", []):
for item in sp.get("items", []):
yield from _yield(item)
for brand in schema.get("vehicle_settings", {}).values():
items = brand.get("items", []) if isinstance(brand, dict) else brand
for item in items:
yield from _yield(item)
def _find_item(schema: dict[str, Any], key: str) -> dict[str, Any] | None:
for item in _walk_items(schema):
if item.get("key") == key:
return item
return None
def _find_section(schema: dict[str, Any], panel_id: str, section_id: str) -> dict[str, Any] | None:
for panel in schema.get("panels", []):
if panel.get("id") != panel_id:
continue
for section in panel.get("sections", []):
if section.get("id") == section_id:
return section
return None
def _flatten_rule_types(rules: list[dict[str, Any]] | None) -> set[str]:
out: set[str] = set()
def _walk(rule: dict[str, Any]) -> None:
out.add(rule.get("type", ""))
if rule.get("type") == "not" and "condition" in rule:
_walk(rule["condition"])
elif rule.get("type") in ("any", "all"):
for c in rule.get("conditions", []):
_walk(c)
for rule in rules or []:
_walk(rule)
return out
def _references_capability_field(rules: list[dict[str, Any]] | None, field: str) -> bool:
found = False
def _walk(rule: dict[str, Any]) -> None:
nonlocal found
if rule.get("type") == "capability" and rule.get("field") == field:
found = True
elif rule.get("type") == "not" and "condition" in rule:
_walk(rule["condition"])
elif rule.get("type") in ("any", "all"):
for c in rule.get("conditions", []):
_walk(c)
for rule in rules or []:
_walk(rule)
return found
def _has_toyota_virtual_cruise_gate(rules: list[dict[str, Any]] | None) -> bool:
def _walk(rule: dict[str, Any]) -> bool:
if rule.get("type") == "all":
conditions = rule.get("conditions", [])
has_capability = any(
c.get("type") == "capability" and c.get("field") == "toyota_virtual_cruise_speed_available" and c.get("equals") is True for c in conditions
)
has_param = any(c.get("type") == "param" and c.get("key") == "ToyotaVirtualCruiseSpeed" and c.get("equals") is True for c in conditions)
if has_capability and has_param:
return True
if rule.get("type") == "not" and "condition" in rule:
return _walk(rule["condition"])
if rule.get("type") in ("any", "all"):
return any(_walk(c) for c in rule.get("conditions", []))
return False
return any(_walk(rule) for rule in rules or [])
def schema():
return generate_schema()
class TestMadsBrandGates(OpenpilotTestCase):
def test_mads_main_cruise_has_brand_gate(self, schema):
"""MadsMainCruiseAllowed must gate on brand and tesla_has_vehicle_bus."""
item = _find_item(schema, "MadsMainCruiseAllowed")
assert item is not None
assert _references_capability_field(item.get("enablement"), "brand")
assert _references_capability_field(item.get("enablement"), "tesla_has_vehicle_bus")
def test_mads_unified_engagement_has_brand_gate(self, schema):
"""MadsUnifiedEngagementMode must mirror MadsMainCruiseAllowed brand-gate."""
item = _find_item(schema, "MadsUnifiedEngagementMode")
assert item is not None
assert _references_capability_field(item.get("enablement"), "brand")
assert _references_capability_field(item.get("enablement"), "tesla_has_vehicle_bus")
class TestTestManeuversSection(OpenpilotTestCase):
def test_lateral_maneuver_mode_in_test_maneuvers(self, schema):
section = _find_section(schema, "developer", "test_maneuvers")
assert section is not None, "developer.test_maneuvers section missing"
keys = {item["key"] for item in section.get("items", [])}
assert "LateralManeuverMode" in keys
assert "LongitudinalManeuverMode" in keys
def test_test_maneuvers_section_requires_attestation(self, schema):
section = _find_section(schema, "developer", "test_maneuvers")
assert section is not None
assert section.get("attestation_required") is True
def test_test_maneuvers_section_visibility_gate(self, schema):
section = _find_section(schema, "developer", "test_maneuvers")
assert section is not None
visibility = section.get("visibility")
assert visibility, "test_maneuvers must have visibility gate"
vis_refs = json.dumps(visibility)
assert "is_development" in vis_refs
assert "is_sp_release" in vis_refs
enablement = section.get("enablement") or []
enable_refs = json.dumps(enablement)
assert "ShowAdvancedControls" in enable_refs, "test_maneuvers must gate ShowAdvancedControls via enablement"
class TestValidator(OpenpilotTestCase):
def test_validator_accepts_real_json(self):
"""settings_ui.json passes the repository's production schema validator."""
self.assertTrue(validate_settings_ui(DEFINITION_PATH))
class TestTorqueOptionGeneration(OpenpilotTestCase):
def test_torque_versions_match_generated_options(self, schema):
versions = _load_torque_versions()
assert versions, "latcontrol_torque_versions.json must have at least one version"
expected = _build_torque_options(versions)
item = _find_item(schema, "TorqueControlTune")
assert item is not None, "TorqueControlTune item must be present"
assert item.get("options") == expected
def test_torque_versions_path_resolves(self):
assert os.path.exists(TORQUE_VERSIONS_PATH), f"latcontrol_torque_versions.json not found at {TORQUE_VERSIONS_PATH}"
class TestReleaseBranchGates(OpenpilotTestCase):
@parameterized.expand(
[
"EnableGithubRunner",
"QuickBootToggle",
],
names=["key"],
)
def test_sp_dev_items_gate_on_is_sp_release(self, schema, key):
"""sunnypilot dev items must hide on sunnypilot release branches (is_sp_release gate)."""
item = _find_item(schema, key)
assert item is not None, f"{key} not found in schema"
rules = (item.get("visibility") or []) + (item.get("enablement") or [])
assert _references_capability_field(rules, "is_sp_release"), f"{key} missing is_sp_release gate"
class TestSpuriousOffroadGatesDropped(OpenpilotTestCase):
def test_disengage_on_accelerator_has_no_offroad_only(self, schema):
item = _find_item(schema, "DisengageOnAccelerator")
assert item is not None
assert "offroad_only" not in _flatten_rule_types(item.get("enablement"))
def test_dynamic_experimental_has_no_offroad_only(self, schema):
item = _find_item(schema, "DynamicExperimentalControl")
assert item is not None
assert "offroad_only" not in _flatten_rule_types(item.get("enablement"))
class TestNotEngagedReplacement(OpenpilotTestCase):
@parameterized.expand(
[
"AlphaLongitudinalEnabled",
"ToyotaEnforceStockLongitudinal",
"ToyotaStopAndGoHack",
],
names=["key"],
)
def test_offroad_only_replaced_with_not_engaged(self, schema, key):
"""These items should use not_engaged, not offroad_only."""
item = _find_item(schema, key)
assert item is not None, f"{key} not found"
rule_types = _flatten_rule_types(item.get("enablement"))
assert "offroad_only" not in rule_types, f"{key} still uses offroad_only"
assert "not_engaged" in rule_types, f"{key} missing not_engaged"
class TestToyotaVirtualCruiseSpeed(OpenpilotTestCase):
def test_vehicle_toggle_contract(self, schema):
toyota = schema["vehicle_settings"]["toyota"]
item = next((item for item in toyota["items"] if item.get("key") == "ToyotaVirtualCruiseSpeed"), None)
assert item is not None
assert item["widget"] == "toggle"
assert item.get("needs_onroad_cycle") is True
assert _references_capability_field(item.get("visibility"), "toyota_virtual_cruise_speed_available")
assert _references_capability_field(item.get("enablement"), "has_longitudinal_control")
assert "not_engaged" in _flatten_rule_types(item.get("enablement"))
def test_custom_acc_section_links_virtual_cruise_opt_in(self, schema):
section = _find_section(schema, "cruise", "custom_acc_increments")
assert section is not None
assert _has_toyota_virtual_cruise_gate(section.get("enablement"))
item = _find_item(schema, "CustomAccIncrementsEnabled")
assert item is not None
assert _has_toyota_virtual_cruise_gate(item.get("enablement"))